Top Banner
Back to Basics: Type Classes Tomer Gabel, Wix August, 2014
35

Scala Back to Basics: Type Classes

Nov 29, 2014

Download

Software

Tomer Gabel

Watch video (in Hebrew): http://parleys.com/play/53f7a9cce4b06208c7b7ca1e

Type classes are a fundamental feature of Scala, which allows you to layer new functionality on top of existing types externally, i.e. without modifying or recompiling existing code. When combined with implicits, this is a truly remarkable tool that enables many of the advanced features offered by the Scala library ecosystem. In this talk we'll go back to basics: how type classes are defined and encoded, and cover several prominent use cases.

A talk given at the Underscore meetup on 19 August, 2014.
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Scala Back to Basics: Type Classes

Back to Basics: Type ClassesTomer Gabel, WixAugust, 2014

Page 2: Scala Back to Basics: Type Classes

THE EXPRESSION PROBLEM

“Define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts).”

-- Philip Wadler

Page 3: Scala Back to Basics: Type Classes

Let’s Build a Calculator

• Operators:– Addition (+)

– Subtraction (-)

– Multiplication (*)

– Division (/)

– Remainder (%)

• Types:– Integers (32-bit signed)

– Longs (64-bit signed)

– Floats (32-bit IEEE 754)

– Longs (64-bit IEEE 754)

Page 4: Scala Back to Basics: Type Classes

Two Views of the Same Problem

Pattern Matchingsealed trait Operand

case class Int32(value: Int) extends Operand

case class Real32(value: Float) extends

Operand

// ...

def addition[T <: Operand](left: T, right:

T): T =

(left, right) match {

case (Int32 (l), Int32 (r)) => Int32 (l +

r)

case (Real32(l), Real32(r)) => Real32(l +

r)

// ...

}

Object Orientedsealed trait Operand

case class Int32(value: Int) extends Operand {

def add(other: Int) = Int32(value + other)

def subtract(other: Int) = Int32(value -

other)

// ...

}

case class Real32(value: Float) extends Operand

{

def add(other: Float) = Real32(value + other)

def subtract(other: Float) = Real32(value -

other)

// ...

}

Page 5: Scala Back to Basics: Type Classes

Two Views of the Same Problem

Pattern Matchingsealed trait Operand

case class Int32(value: Int) extends Operand

case class Real32(value: Float) extends

Operand

// ...

def addition[T <: Operand](left: T, right:

T): T =

(left, right) match {

case (Int32 (l), Int32 (r)) => Int32 (l +

r)

case (Real32(l), Real32(r)) => Real32(l +

r)

// ...

}

Object Orientedsealed trait Operand

case class Int32(value: Int) extends Operand {

def add(other: Int) = Int32(value + other)

def subtract(other: Int) = Int32(value -

other)

// ...

}

case class Real32(value: Float) extends Operand

{

def add(other: Float) = Real32(value + other)

def subtract(other: Float) = Real32(value -

other)

// ...

}

What if we

wanted to add a

type?

Page 6: Scala Back to Basics: Type Classes

Two Views of the Same Problem

Pattern Matchingsealed trait Operand

case class Int32(value: Int) extends Operand

case class Real32(value: Float) extends

Operand

// ...

def addition[T <: Operand](left: T, right:

T): T =

(left, right) match {

case (Int32 (l), Int32 (r)) => Int32 (l +

r)

case (Real32(l), Real32(r)) => Real32(l +

r)

// ...

}

Object Orientedsealed trait Operand

case class Int32(value: Int) extends Operand {

def add(other: Int) = Int32(value + other)

def subtract(other: Int) = Int32(value -

other)

// ...

}

case class Real32(value: Float) extends Operand

{

def add(other: Float) = Real32(value + other)

def subtract(other: Float) = Real32(value -

other)

// ...

}

What if we

wanted to add a

type?

What if we

wanted to add an

operator?

Page 7: Scala Back to Basics: Type Classes

TYPE CLASSES TO THE RESCUE

Page 8: Scala Back to Basics: Type Classes

What’s a Type Class?

• A type class:– Enables ad-hoc polymorphism– Statically typed (i.e. type-safe)– Borrowed from Haskell

• Solves the expression problem:– Behavior can be extended– … at compile-time– ... after the fact– … without changing/recompiling

existing code

Page 9: Scala Back to Basics: Type Classes

Example #1: Equality

• Scala inherits legacy aspects of Java– This includes AnyRef.equals:

def equals( other: AnyRef ): Boolean

– So the following compiles:3.14159265359 == "pi" // Evaluates to false

• What if we wanted to implement type-safe equality?– Let’s define a type-safe isEqual function:

isEqual( 3.14159265359, "pi” ) // Does not compile!

Page 10: Scala Back to Basics: Type Classes

What’s in a Type Class?

• Three components are required:– A signature– Implementations for

supported types– A function that requires

a type class This is where things get hairy.

Page 11: Scala Back to Basics: Type Classes

Slight Digression

• A method in Scala can have multiple parameter lists:

def someMethod( x: Int )( y: String )( z: Double ): Unit = { println( s"x=$x, y=$y, z=$z" )}

scala> someMethod( 10 )( "abc" )( scala.math.Pi )x=10, y=abc, z=3.141592653589793

• There are multiple uses for this, but the most important is…

Page 12: Scala Back to Basics: Type Classes

Scala Implicits

• The last parameter list of a method can be marked implicit

• Implicit parameters are filled in by the compiler

– In effect, you require evidence of the compiler

– … such as the existence of a type class in scope

– You can also specify parameters explicitly, if needed

Page 13: Scala Back to Basics: Type Classes

Putting it together

• Let’s define our type class:trait Equality[ L, R ] { def equals( left: L, right: R ): Boolean }

• … and our isEqual function:def isEqual[ L, R ]( left: L, right: R ) ( implicit ev: Equality[ L, R ] ): Boolean = ev.equals( left, right )

This is where the magic happens

Page 14: Scala Back to Basics: Type Classes

Still missing something!

• We have no implementations of the Equality trait, so nothing works!scala> isEqual( 3, 3 )<console>:10: error: could not find implicit value for parameter ev: Equality[Int,Int]

• We need to implement Equality[ T, T ]:implicit def sameTypeEquality[ T ] = new Equality[ T, T ] { def equals( left: T, right: T ) = left.equals( right )}

• And now it works:scala> isEqual( 3, 3 )res1: Boolean = true

Page 15: Scala Back to Basics: Type Classes

Ad-hoc Polymorphism

• Now we’ve met our original goal:scala> isEqual( 3.14159265359, "pi" )<console>:11: error: could not find implicit value for parameter ev: Equality[Double,String]

• But what if we wanted to equate doubles and strings?• Well then, let’s add another implementation!

implicit object DoubleEqualsString extends Equality[ Double, String ] { def equals( left: Double, right: String ) = left.toString == right }

• Et voila, no recompilation or code changes needed:scala> isEqual( 3.14159265359, "pi" )res5: Boolean = false

Page 16: Scala Back to Basics: Type Classes

QUESTIONS SO FAR

Page 17: Scala Back to Basics: Type Classes

Example #2: Sort Me, Maybe

• Let’s implement a sort function (e.g. bubble sort)

• With one caveat:– It should operate on any type– … for which an ordering exists

• Obviously, we’ll use type classes!

Page 18: Scala Back to Basics: Type Classes

Possible Solution

trait Ordering[ T ] { def isLessThan( left: T, right: T ): Boolean }

def sort[ T ]( items: Seq[ T ] )( implicit ord: Ordering[ T ] ): Seq[ T ] = { val buffer = mutable.ArrayBuffer( items:_* )

for ( i <- 0 until items.size; j <- ( i + 1 ) until items.size ) if ( ord.isLessThan( buffer( j ), buffer( i ) ) ) { val temp = buffer( i ) buffer( i ) = buffer( j ) buffer( j ) = temp }

buffer}

Page 19: Scala Back to Basics: Type Classes

Possible Solution, cont.

• Sample implementation for integers:

implicit object IntOrdering extends Ordering[ Int ] { def isLessThan( left: Int, right: Int ) = left < right}

val numbers = Seq( 4, 1, 10, 8, 14, 2 )Assert( sort( numbers ) == Seq( 1, 2, 4, 8, 10, 14 ) )

Page 20: Scala Back to Basics: Type Classes

Possible Solution, cont.

• Sample implementation for a domain entity:

case class Person( name: String, age: Int )

implicit object PersonOrdering extends Ordering[ Person ] { def isLessThan( left: Person, right: Person ) = left.age < right.age}

val haim = Person( "Haim", 12 )val dafna = Person( "Dafna", 20 )val ofer = Person( "Ofer", 1 )assert( sort( Seq( haim, dafna, ofer ) ) == Seq( ofer, haim, dafna ) )

Page 21: Scala Back to Basics: Type Classes

Implicit Search Order

Current Scope• Defined implicits• Explicit imports• Wildcard imports

Companion• … of T• … of supertypes of T

Outer Scope• Enclosing class

Page 22: Scala Back to Basics: Type Classes

REAL WORLD EXAMPLES, PLEASE?

Page 23: Scala Back to Basics: Type Classes

Example #3: Server Pipeline

• REST is good, but annoying to write. Let’s simplify:

case class DTO( message: String )

class MyServlet extends NicerHttpServlet { private val counter = new AtomicInteger( 0 )

get( "/service" ) { counter.incrementAndGet() DTO( "hello, world!" ) }

get( "/count" ) { counter.get() }}

Uses return value;no direct response manipulation

Page 24: Scala Back to Basics: Type Classes

Example #3: Server Pipeline

• What’s in a server?– Routing– Rendering– Error handling

• Let’s focus on rendering:

trait ResponseRenderer[ T ] { def render( value : T, request : HttpServletRequest, response: HttpServletResponse ): Unit}

Page 25: Scala Back to Basics: Type Classes

Example #3: Server Pipeline

• A couple of basic renderers:

implicit object StringRenderer extends ResponseRenderer[ String ] { def render( value: String, request: HttpServletRequest, response: HttpServletResponse ) = { val w = response.getWriter try w.write( value ) finally w.close() }}

implicit object IntRenderer extends ResponseRenderer[ Int ] { def render( value: Int, request: HttpServletRequest, response: HttpServletResponse ) = implicitly[ ResponseRenderer[ String ] ].render( value.toString, request, response )}

Page 26: Scala Back to Basics: Type Classes

Example #3: Server Pipeline

• Putting it together:

trait NicerHttpServlet extends HttpServlet {

private trait Handler { type Response def result: Response def renderer: ResponseRenderer[ Response ] }

private var handlers: Map[ String, Handler ] = Map.empty

protected def get[ T : ResponseRenderer ]( url: String )( thunk: => T ) = handlers += url -> new Handler { type Response = T def result = thunk def renderer = implicitly[ ResponseRenderer[ T ] ] }

Page 27: Scala Back to Basics: Type Classes

Example #3: Server Pipeline

• And finally:

override def doGet( req: HttpServletRequest, resp: HttpServletResponse ) = handlers.get( req.getRequestURI ) match { case None => resp.sendError( HttpServletResponse.SC_NOT_FOUND )

case Some( handler ) => try handler.renderer.render( handler.result, req, resp ) catch { case e: Exception => resp.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ) } }

Page 28: Scala Back to Basics: Type Classes

PHEW.Take a deep breath

Page 29: Scala Back to Basics: Type Classes

Example #4: JSON Serialization

• Assume we already have a good model for JSON

• How do we add type-safe serialization?

Page 30: Scala Back to Basics: Type Classes

Example #4: JSON Serialization

• Let’s start with a typeclass:

trait JsonSerializer[ T ] { def serialize( value: T ): JsonValue def deserialize( value: JsonValue ): T}

• And the corresponding library signature:

def serialize[ T ]( instance: T )( implicit ser: JsonSerializer[ T ] ) = ser.serialize( instance )def deserialize[ T ]( json: JsonValue )( implicit ser: JsonSerializer[ T ] ) = ser.deserialize( json )

Page 31: Scala Back to Basics: Type Classes

Example #4: JSON Serialization

• Define a few basic serializers…

implicit object BooleanSerializer extends JsonSerializer[ Boolean ] { def serialize( value: Boolean ) = JsonBoolean( value ) def deserialize( value: JsonValue ) = value match { case JsonBoolean( bool ) => bool case other => error( other ) }}

implicit object StringSerializer extends JsonSerializer[ String ] { def serialize( value: String ) = JsonString( value ) def deserialize( value: JsonValue ) = value match { case JsonString( string ) => string case other => error( other ) }}

Page 32: Scala Back to Basics: Type Classes

Example #4: JSON Serialization

• We can also handle nested structures– The compiler resolves typeclasses recursively!

• For example, Option[ T ] :

implicit def optionSerializer[ T ]( implicit ser: JsonSerializer[ T ] ) = new JsonSerializer[ Option[ T ] ] {

def serialize( value: Option[ T ] ) = value map ser.serialize getOrElse JsonNull

def deserialize( value: JsonValue ) = value match { case JsonNull => None case other => Some( ser.deserialize( other ) ) } }

Require a serializer for T

… and delegate to it

Page 33: Scala Back to Basics: Type Classes

Example #4: JSON Serialization

• What about an arbitrary type?

case class Person( name: String, surname: String, age: Int )

implicit object PersonSerializer extends JsonSerializer[ Person ] { def serialize( value: Person ) = JsonObject( JsonField( "name", serialize( value.name ) ), JsonField( "surname", serialize( value.surname ) ), JsonField( "age", serialize( value.age ) ) ) def deserialize( value: JsonValue ) = value match { case obj: JsonObject => Person( name = deserialize[ String ]( obj \ "name" ), surname = deserialize[ String ]( obj \ "surname" ), age = deserialize[ Int ]( obj \ "age" ) ) case _ => error( value ) }}

Page 34: Scala Back to Basics: Type Classes

Summary

• We added serialization for Person after the fact without…

– … modifying the serialization framework

– … modifying the domain object

• We did not compromise:

– … type safety or performance

– … modularity or encapsulation

• This applies everywhere!

clients of either are unaffected!

Page 35: Scala Back to Basics: Type Classes

… and we’re done

• Thank you for your time!

• Questions/comments?– [email protected]– @tomerg– http://www.tomergabel.com

• Code samples:– http://git.io/aWc9eQ