Type classes 101
Classification beyond inheritance
Alexey Raga
String
Int
List[Boolean]
case class Name(value: String)
case class Age(value: Int)
case class Person(name: Name, age: Age)
case class Gang(leader: Person, members: List[Person])
Types classify data
String
Int
List[Boolean]
case class Name(value: String)
case class Age(value: Int)
case class Person(name: Name, age: Age)
case class Gang(leader: Person, members: List[Person])
Types classify data
public class Person
implements ISerialisable
{
public String name;
public String address;
...
}
public void saveToDisk(ISerialisable obj) { … }
Types classify data
Types classify data
public class Person
implements ISerialisable, IJsonSerialisable, IXmlSerialisable, IPrettyPrint
{
public String name;
public String address;
...
}
Expression problem
trait Expr
case class Lit(value: Int) extends Expr
case class Add(x: Expr, y: Expr) extends Expr
val expr = Add(Lit(15), Lit(6))
Expression problem
● Operation extension
add new operations: eval, prettyPrint, etc.
● Data extension
add new expressions: Mul, Pow, Neg, ec.
● Static type safety
no isInstanceOf / asInstanceOf
Expression problem
● Operation extension
add new operations: eval, prettyPrint, etc.
● Data extension
add new expressions: Mul, Pow, Neg, ec.
● Static type safety
no isInstanceOf / asInstanceOf
Expression problem
trait Expr {
def eval: Int
def print: String
}
case class Lit(value: Int) extends Expr {
def eval = ???
def print = ???
}
case class Add(x: Expr, y: Expr) extends Expr {
def eval = ???
def print = ???
}
Expression problem
trait Expr {
def eval: Int = this match {
case Lit => ???
case Add => ???
}
def print: String = this match {
case Lit => ???
case Add => ???
}
}
case class Lit(value: Int) extends Expr
case class Add(x: Expr, y: Expr) extends Expr
We can do better
Classification
Plants Animals
?
Classification
Fungi
Classification
Classification
Classification
Classifying types
trait Serialisable[A] {
def serialise(obj: A) : Array[Byte]
}
object PersonSerialisable extends Serialisable[Person] {
def serialise(obj: Person): Array[Byte] = ???
}
def saveToDisk[A](obj: A, ser: Serialisable[A]) = {
val data = ser.serialise(obj)
???
}
saveToDisk(Person("john", 99), PersonSerialisable)
Type classes classify types
trait Serialisable[A] {
def serialise(obj: A) : Array[Byte]
}
implicit object PersonSerialisable extends Serialisable[Person] {
def serialise(obj: Person): Array[Byte] = ???
}
def saveToDisk[A](obj: A)(implicit ser: Serialisable[A]) = {
val data = ser.serialise(obj)
???
}
saveToDisk(Person("john", 99))
Type classes classify types
// already defined in Scala
// def implicitly[T](implicit e: T) = e
def saveToDisk[A: Serialisable](obj: A) = {
val ser = implicitly[Serialisable[A]]
val data = ser.serialise(obj)
...
}
saveToDisk(Person("john", 99))
Testimony ;)
Just saying...
import serialisation.json._
//import serialisation.csv._
//import serialisation.xml._
def saveToDisk[A](obj: A)(implicit ser: Serialisable[A]) = {
val data = ser.serialise(obj)
???
}
saveToDisk(Person("john", 99))
Type classes in Scala
trait Ordering[T] {
def compare(x : T, y : T) : Int
def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0
def gteq(x : T, y : T) : Boolean = compare(x, y) => 0
...
}
trait Numeric[T] extends Ordering[T] {
def plus(x : T, y : T) : T
def minus(x : T, y : T) : T
def negate(x : T) : T
...
}
Type classes in Scala
trait Ordering[T] {
def compare(x : T, y : T) : Int
def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0
def gteq(x : T, y : T) : Boolean = compare(x, y) => 0
}
trait Numeric[T] extends Ordering[T] {
def plus(x : T, y : T) : T
def minus(x : T, y : T) : T
def negate(x : T) : T
}
trait TraversableOnce[+A] {
def sum[B >: A](implicit num : scala.Numeric[B]) : B = ???
def min[B >: A](implicit cmp : scala.Ordering[B]) : A = ???
def max[B >: A](implicit cmp : scala.Ordering[B]) : A = ???
}
Type classes in Scala
trait Ordering[T] {
def compare(x : T, y : T) : Int
def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0
def gteq(x : T, y : T) : Boolean = compare(x, y) => 0
}
trait Numeric[T] extends Ordering[T] {
def plus(x : T, y : T) : T
def minus(x : T, y : T) : T
def negate(x : T) : T
}
trait TraversableOnce[+A] {
def sum[B >: A](implicit num : scala.Numeric[B]) : B = ???
def min[B >: A](implicit cmp : scala.Ordering[B]) : A = ???
def max[B >: A](implicit cmp : scala.Ordering[B]) : A = ???
}
val sum = List(1,2,3).sum
val min = List(1,2,3).min
Type classes in Scalaz
trait Equal[A] {
def equal(a1 : A, a2 : A) : Boolean
}
trait Show[A] {
def shows(a : A) : String
}
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
trait Semigroup[A] {
def append(a1 : A, a2 : => A) : A
}
trait Monoid[A] extends Semigroup[A] {
def zero : A
}
Deriving proofs
//tuple of Equals is also an Equal
implicit def tuple2Equal[A: Equal, B: Equal]: Equal[(A, B)] =
new Equal[(A, B)] {
def equal(a1: (A, B), a2: (A, B)) : Boolean =
a1._1 === a2._1 && a1._2 === a2._2
}
//tuple of Semigroups is also a Semigroup
implicit def tuple2Semigroup[A: Semigroup, B: Semigroup]: Semigroup[(A, B)] = {
new Semigroup[(A, B)] {
def append(p1: (A, B), p2: => (A, B)) =
((p1._1 |+| p2._1), (p1._2 |+| p2._2))
}
}
Expression problem
package ep
trait Expr
case class Lit(value: Int) extends Expr
case class Add[A <: Expr, B <: Expr](x: A, y: B) extends Expr
Expression problem
● Operation extension
add new operations: eval, prettyPrint, etc.
● Data extension
add new expressions: Mul, Pow, Neg, ec.
● Static type safety
no isInstanceOf / asInstanceOf
Expression problem
package ep.evaluate
import ep._
trait Eval[A <: Expr] {
def eval(expr: A) : Int
}
object Eval {
def evaluate[A <: Expr](expr: A)(implicit evA: Eval[A]) = evA.eval(expr)
implicit object LitEval extends Eval[Lit] { def eval(expr: Lit) = expr.value }
implicit def addEval[A <: Expr, B <: Expr](implicit evA: Eval[A], evB: Eval[B]) = {
new Eval[Add[A, B]] {
def eval(expr: Add[A, B]) = evA.eval(expr.x) + evB.eval(expr.y)
}
}
}
Expression problem
package ep
import evaluate._
Eval.evaluate( Add(Lit(15), Lit(6)) ) === 21
Expression problem
package ep.expressions
import ep._
import evaluate._
case class Mul[A <: Expr, B <: Expr](x: A, y: B) extends Expr
object Mul {
implicit def mulEval[A <: Expr, B <: Expr](implicit evA: Eval[A], evB: Eval[B]) = {
new Eval[Mul[A, B]] {
def eval(expr: Mul[A, B]) = evA.eval(expr.x) * evB.eval(expr.y)
}
}
}
Expression problem
import evaluate._
import expressions._
Eval.evaluate( Mul(Lit(2), Add(Lit(15), Lit(6))) ) === 42
Expression problem
package ep.operations
import ep._
import ep.expressions._
trait PPrint[A <: Expr] {
def print(expr: A) : String
}
object PPrint {
def prettyPrint[A <: Expr](expr: A)(implicit pa: PPrint[A]) = pa.print(expr)
implicit object LitPrint extends PPrint[Lit] {
def print(expr: Lit) = expr.value.toString
}
implicit def mulPrint[A <: Expr, B <: Expr](implicit pA: PPrint[A], pB: PPrint[B]) = {
new PPrint[Mul[A, B]] {
def print(expr: Mul[A, B]) = pA.print(expr.x) + " * " + pB.print(expr.y)
}
}
Expression problem
import expressions._
import operations._
import evaluate._
PPrint.prettyPrint( Mul(Lit(2), Add(Lit(15), Lit(6))) ) === "2 * (15 + 6) = 42"
Expression problem
● Operation extension
add new operations: eval, prettyPrint, etc.
● Data extension
add new expressions: Mul, Pow, Neg, ec.
● Static type safety
no isInstanceOf / asInstanceOf
● Add behaviours retroactively
No need to change existing data types
● Solution to the Expression problem
Operation and data extension with static type safety
● Different kinds of operations
“instance” (A => String), “factory” (String => A), etc.
What about us?
Isn't it enough?
No we're not in paradise
This is who we are
This is what we've got
No it's not our paradise

Type classes 101 - classification beyond inheritance

  • 1.
    Type classes 101 Classificationbeyond inheritance Alexey Raga
  • 2.
    String Int List[Boolean] case class Name(value:String) case class Age(value: Int) case class Person(name: Name, age: Age) case class Gang(leader: Person, members: List[Person]) Types classify data
  • 3.
    String Int List[Boolean] case class Name(value:String) case class Age(value: Int) case class Person(name: Name, age: Age) case class Gang(leader: Person, members: List[Person]) Types classify data
  • 4.
    public class Person implementsISerialisable { public String name; public String address; ... } public void saveToDisk(ISerialisable obj) { … } Types classify data
  • 5.
    Types classify data publicclass Person implements ISerialisable, IJsonSerialisable, IXmlSerialisable, IPrettyPrint { public String name; public String address; ... }
  • 6.
    Expression problem trait Expr caseclass Lit(value: Int) extends Expr case class Add(x: Expr, y: Expr) extends Expr val expr = Add(Lit(15), Lit(6))
  • 7.
    Expression problem ● Operationextension add new operations: eval, prettyPrint, etc. ● Data extension add new expressions: Mul, Pow, Neg, ec. ● Static type safety no isInstanceOf / asInstanceOf
  • 8.
    Expression problem ● Operationextension add new operations: eval, prettyPrint, etc. ● Data extension add new expressions: Mul, Pow, Neg, ec. ● Static type safety no isInstanceOf / asInstanceOf
  • 9.
    Expression problem trait Expr{ def eval: Int def print: String } case class Lit(value: Int) extends Expr { def eval = ??? def print = ??? } case class Add(x: Expr, y: Expr) extends Expr { def eval = ??? def print = ??? }
  • 10.
    Expression problem trait Expr{ def eval: Int = this match { case Lit => ??? case Add => ??? } def print: String = this match { case Lit => ??? case Add => ??? } } case class Lit(value: Int) extends Expr case class Add(x: Expr, y: Expr) extends Expr
  • 11.
    We can dobetter
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
    Classifying types trait Serialisable[A]{ def serialise(obj: A) : Array[Byte] } object PersonSerialisable extends Serialisable[Person] { def serialise(obj: Person): Array[Byte] = ??? } def saveToDisk[A](obj: A, ser: Serialisable[A]) = { val data = ser.serialise(obj) ??? } saveToDisk(Person("john", 99), PersonSerialisable)
  • 18.
    Type classes classifytypes trait Serialisable[A] { def serialise(obj: A) : Array[Byte] } implicit object PersonSerialisable extends Serialisable[Person] { def serialise(obj: Person): Array[Byte] = ??? } def saveToDisk[A](obj: A)(implicit ser: Serialisable[A]) = { val data = ser.serialise(obj) ??? } saveToDisk(Person("john", 99))
  • 19.
    Type classes classifytypes // already defined in Scala // def implicitly[T](implicit e: T) = e def saveToDisk[A: Serialisable](obj: A) = { val ser = implicitly[Serialisable[A]] val data = ser.serialise(obj) ... } saveToDisk(Person("john", 99))
  • 20.
  • 23.
    Just saying... import serialisation.json._ //importserialisation.csv._ //import serialisation.xml._ def saveToDisk[A](obj: A)(implicit ser: Serialisable[A]) = { val data = ser.serialise(obj) ??? } saveToDisk(Person("john", 99))
  • 24.
    Type classes inScala trait Ordering[T] { def compare(x : T, y : T) : Int def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0 def gteq(x : T, y : T) : Boolean = compare(x, y) => 0 ... } trait Numeric[T] extends Ordering[T] { def plus(x : T, y : T) : T def minus(x : T, y : T) : T def negate(x : T) : T ... }
  • 25.
    Type classes inScala trait Ordering[T] { def compare(x : T, y : T) : Int def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0 def gteq(x : T, y : T) : Boolean = compare(x, y) => 0 } trait Numeric[T] extends Ordering[T] { def plus(x : T, y : T) : T def minus(x : T, y : T) : T def negate(x : T) : T } trait TraversableOnce[+A] { def sum[B >: A](implicit num : scala.Numeric[B]) : B = ??? def min[B >: A](implicit cmp : scala.Ordering[B]) : A = ??? def max[B >: A](implicit cmp : scala.Ordering[B]) : A = ??? }
  • 26.
    Type classes inScala trait Ordering[T] { def compare(x : T, y : T) : Int def lteq(x : T, y : T) : Boolean = compare(x, y) <= 0 def gteq(x : T, y : T) : Boolean = compare(x, y) => 0 } trait Numeric[T] extends Ordering[T] { def plus(x : T, y : T) : T def minus(x : T, y : T) : T def negate(x : T) : T } trait TraversableOnce[+A] { def sum[B >: A](implicit num : scala.Numeric[B]) : B = ??? def min[B >: A](implicit cmp : scala.Ordering[B]) : A = ??? def max[B >: A](implicit cmp : scala.Ordering[B]) : A = ??? } val sum = List(1,2,3).sum val min = List(1,2,3).min
  • 27.
    Type classes inScalaz trait Equal[A] { def equal(a1 : A, a2 : A) : Boolean } trait Show[A] { def shows(a : A) : String } trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } trait Semigroup[A] { def append(a1 : A, a2 : => A) : A } trait Monoid[A] extends Semigroup[A] { def zero : A }
  • 28.
    Deriving proofs //tuple ofEquals is also an Equal implicit def tuple2Equal[A: Equal, B: Equal]: Equal[(A, B)] = new Equal[(A, B)] { def equal(a1: (A, B), a2: (A, B)) : Boolean = a1._1 === a2._1 && a1._2 === a2._2 } //tuple of Semigroups is also a Semigroup implicit def tuple2Semigroup[A: Semigroup, B: Semigroup]: Semigroup[(A, B)] = { new Semigroup[(A, B)] { def append(p1: (A, B), p2: => (A, B)) = ((p1._1 |+| p2._1), (p1._2 |+| p2._2)) } }
  • 30.
    Expression problem package ep traitExpr case class Lit(value: Int) extends Expr case class Add[A <: Expr, B <: Expr](x: A, y: B) extends Expr
  • 31.
    Expression problem ● Operationextension add new operations: eval, prettyPrint, etc. ● Data extension add new expressions: Mul, Pow, Neg, ec. ● Static type safety no isInstanceOf / asInstanceOf
  • 32.
    Expression problem package ep.evaluate importep._ trait Eval[A <: Expr] { def eval(expr: A) : Int } object Eval { def evaluate[A <: Expr](expr: A)(implicit evA: Eval[A]) = evA.eval(expr) implicit object LitEval extends Eval[Lit] { def eval(expr: Lit) = expr.value } implicit def addEval[A <: Expr, B <: Expr](implicit evA: Eval[A], evB: Eval[B]) = { new Eval[Add[A, B]] { def eval(expr: Add[A, B]) = evA.eval(expr.x) + evB.eval(expr.y) } } }
  • 33.
    Expression problem package ep importevaluate._ Eval.evaluate( Add(Lit(15), Lit(6)) ) === 21
  • 34.
    Expression problem package ep.expressions importep._ import evaluate._ case class Mul[A <: Expr, B <: Expr](x: A, y: B) extends Expr object Mul { implicit def mulEval[A <: Expr, B <: Expr](implicit evA: Eval[A], evB: Eval[B]) = { new Eval[Mul[A, B]] { def eval(expr: Mul[A, B]) = evA.eval(expr.x) * evB.eval(expr.y) } } }
  • 35.
    Expression problem import evaluate._ importexpressions._ Eval.evaluate( Mul(Lit(2), Add(Lit(15), Lit(6))) ) === 42
  • 36.
    Expression problem package ep.operations importep._ import ep.expressions._ trait PPrint[A <: Expr] { def print(expr: A) : String } object PPrint { def prettyPrint[A <: Expr](expr: A)(implicit pa: PPrint[A]) = pa.print(expr) implicit object LitPrint extends PPrint[Lit] { def print(expr: Lit) = expr.value.toString } implicit def mulPrint[A <: Expr, B <: Expr](implicit pA: PPrint[A], pB: PPrint[B]) = { new PPrint[Mul[A, B]] { def print(expr: Mul[A, B]) = pA.print(expr.x) + " * " + pB.print(expr.y) } }
  • 37.
    Expression problem import expressions._ importoperations._ import evaluate._ PPrint.prettyPrint( Mul(Lit(2), Add(Lit(15), Lit(6))) ) === "2 * (15 + 6) = 42"
  • 38.
    Expression problem ● Operationextension add new operations: eval, prettyPrint, etc. ● Data extension add new expressions: Mul, Pow, Neg, ec. ● Static type safety no isInstanceOf / asInstanceOf
  • 39.
    ● Add behavioursretroactively No need to change existing data types ● Solution to the Expression problem Operation and data extension with static type safety ● Different kinds of operations “instance” (A => String), “factory” (String => A), etc.
  • 42.
    What about us? Isn'tit enough? No we're not in paradise This is who we are This is what we've got No it's not our paradise