April 07, 2017
150

# Typeclasses — a type system construct

Presented at Scala, Warsaw - http://scalar-conf.com

April 07, 2017

## Transcript

1. ### TYPECLASSES a type system construct SCALAR, WARSAW, POLAND - 7.4.2017

ANDREA LATTUADA — @utaal PHD CANDIDATE / RA, SYSTEMS GROUP, DEPT. OF COMPUTER SCIENCE, ETH ZÜRICH 1
2. ### 1. why typeclasses 2. as first-class citizens, encoded 3. complications

arising from encoding 4. case studies, good practices 2

4. ### case class Square(side: Float) case class Rectangle(a: Float, b: Float)

case class Circle(radius: Float) def area(square: Square): Float = Math.pow(square.side, 2).toFloat def area(rectangle: Rectangle): Float = rectangle.a * rectangle.b def area(circle: Circle): Float = (Math.PI * Math.pow(circle.radius, 2)).toFloat def compareArea[ A if there's a function area(A):Float, B if there's a function area(B):Float, ](a: A, b: B): Float = area(a) - area(b) compareArea(Square(10), Circle(5)) 4
5. ### GROUP TYPES THAT support certain operations, WITH SUBTYPING // define

operations trait HasArea { def area: Float } // types that support the operations defined case class Square(side: Float) extends HasArea { def area = Math.pow(square.side, 2).toFloat } case class Rectangle(a: Float, b: Float) extends HasArea { def area = this.a * this.b } def compareArea(a: HasArea, b: HasArea): Float = a.area - b.area 5
6. ### GROUP TYPES FOR WHICH certain operations are supported, WITH TYPECLASSES

Square, Rectangle, Circle support area def area(square: Square): Float = ... def area(rectangle: Rectangle): Float = ... def area(circle: Circle): Float = ... defined ad-hoc (specifically for certain types, and independently from their definition) 6
7. ### // define operations typeclass HasArea[T] { def area(t: T): Float

} def compareArea[A: HasArea, B: HasArea](a: A, b: B): Float = area(a) - area(b) Type variables A and B can only be instantiated to a type whose members support the operations associated with typeclass HasArea.1 1 https://en.wikipedia.org/wiki/Type_class 7
8. ### 1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO

THE "GROUP" THAT SUPPORTS THESE OPERATIONS 8
9. ### 1. DEFINE supported operations FOR A TYPECLASS -- Haskell class

HasArea a where area :: a -> Float // Scala, almost typeclass HasArea[T] { def area(t: T): Float } 9
10. ### 2. ADD TYPE TO THE "GROUP" BY PROVIDING AN INSTANCE

(implementation) -- Haskell data Square = Square { side :: Float } deriving Show data Rectangle = Rectangle { a :: Float, b :: Float } deriving Show data Circle = Circle { radius :: Float } deriving Show class HasArea a where area :: a -> Float instance HasArea Square where -- instance of HasArea for Square area x = side x ^^ 2 instance HasArea Rectangle where -- instance of HasArea for Rectangle area x = a x * b x instance HasArea Circle where -- instance of HasArea for Circle area x = pi * radius x ^^ 2 10
11. ### WE CAN restrict A TYPE VARIABLE TO TYPES BELONGING TO

A CERTAIN TYPECLASS -- Haskell class HasArea a where area :: a -> Float ... compareArea :: HasArea a => HasArea b => a -> b -> Float compareArea x y = area x - area y 11
12. ### data Square = Square { side :: Float } deriving

Show data Rectangle = Rectangle { a :: Float, b :: Float } deriving Show data Circle = Circle { radius :: Float } deriving Show class HasArea a where area :: a -> Float instance HasArea Square where area x = side x ^^ 2 instance HasArea Rectangle where area x = a x * b x instance HasArea Circle where area x = pi * radius x ^^ 2 compareArea :: HasArea a => HasArea b => a -> b -> Float compareArea a b = area a - area b *Main> compareArea Circle { radius = 3 } Rectangle { a = 2, b = 3 } 22.274334 12

14. ### IMPLICITS class Something[A] implicit val implicitSomething: Something[Int] = new Something[Int]()

// from standard library def implicitly[T](implicit e: T): T scala> implicitly[Something[Int]] res1: Something[Int] = Something@5680a178 14
15. ### 1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO

THE "GROUP" THAT SUPPORTS THESE OPERATIONS 15
16. ### 1. TYPECLASS deﬁnition IN SCALA -- Haskell class HasArea a

where area :: a -> Float ‑ // Scala trait HasArea[A] { def area(x: A): Float } 16
17. ### 2. TYPECLASS instance IN SCALA data Square = Square {

side :: Float } deriving Show instance HasArea Square where area x = side x ^^ 2 ‑ case class Square(side: Float) implicit val squareHasArea: HasArea[Square] = new HasArea[Square] { def area(x: Square): Float = Math.pow(x.side, 2).toFloat } 17
18. ### TYPE constraint IN SCALA compareArea :: HasArea a => HasArea

b => a -> b -> Float compareArea a b = area a - area b ‑ def compareArea[A, B](a: A, b: B) (implicit aHasArea: HasArea[A], bHasArea: HasArea[B]): Float = aHasArea.area(a) - bHasArea.area(b) // or, equivalently (desugars to equivalent form) def compareArea[A: HasArea, B: HasArea](a: A, b: B): Float = implicitly[HasArea[A]].area(a) - implicitly[HasArea[B]].area(b) 18
19. ### TYPECLASS syntax IN SCALA area \$ Square { side =

3.5 } ‑ implicit class areaOps[A: HasArea](a: A) { def area: Float = implicitly[HasArea[A]].area(a) } scala> Square(side = 3.5).area 19
20. ### trait HasArea[A] { def area(x: A): Float } implicit val

squareHasArea: HasArea[Square] = new HasArea[Square] { def area(x: Square): Float = Math.pow(x.side, 2).toFloat } // more implementations implicit class areaOps[A: HasArea](a: A) { def area: Float = implicitly[HasArea[A]].area(a) } scala> Square(side = 3.5).area def compareArea[A: HasArea, B: HasArea](a: A, b: B): Float = a.area - b.area 20

23. ### ORPHAN INSTANCES An orphan instance is a type class instance

for class C and type T which is neither defined in the module where C is defined nor in the module where T is defined.2 2 https://wiki.haskell.org/Orphan_instance 23
24. ### HASKELL - INSTANCE SCOPE ▸ open world assumption ▸ instances

imported when importing a module that defines them or imports them (no hiding possible) — even when the typeclass is not in scope 24
25. ### RUST - INSTANCE SCOPE ▸ instances can only be defined

in the same file as the datatype or the typeclass (no orphan instances) ▸ instances available when the trait is imported 25
26. ### SCALA - INSTANCE SCOPE based on implicit resolution ▸ most

specific first ▸ search in implicit scope and companion objects 26
27. ### SCALA: MOST SPECIFIC FIRST trait SomeTypeclass[T] { val where: String

} // wildcard instance implicit def genericSomeTypeclass[T]: SomeTypeclass[T] = new SomeTypeclass[T] { val where = "generic" } implicit val someTypeclassString: SomeTypeclass[String] = new SomeTypeclass[String] { val where = "specific" } > implicitly[SomeTypeclass[String]].where res1: String = "specific" 27
28. ### SCALA: DIFFERENT ORIGIN 1. (implicit scope) local scope, imported or

inherited values/defs; if one is available in both, ambiguous implicit ▸ among imported or inherited values/defs, the one defined in the most specific type 2. companion object of the datatype or typeclass 28
29. ### SCALA: ISSUES ▸ (readability) where is this instance coming from?

▸ (discoverability) where do I find available instances for a certain typeclass? ▸ (coherence, maintainability) what instance am I using when multiple are available? 29
30. ### SCALA: Coherence AND MAINTANABILITY object HasArea { implicit val rectangleHasArea

= new HasArea[Rectangle] { def area(r: Rectangle): Double = r.a * r.b } } ꔇ class Module { implicit sphericalRectangleHasArea = new HasArea[Rectangle] { def area(r: Rectangle): Double = // ... } compareArea(Rectangle(10, 12), Rectangle(13, 14)) } 30
31. ### Coherence: BLANKET IMPLEMENTATIONS trait RootJsonCodec[T] { def encodeToJson(t: T): JsonValue

} trait DbCodec[T] { def encodeToDbValue(t: T): DbValue } implicit def dbCodecRootJsonCodec[A: RootJsonCodec]: DbCodec[T] 31
32. ### Coherence: overlapping BLANKET IMPLEMENTATIONS trait RootJsonCodec[T] { ... } trait

BinaryCodec[T] { ... } trait DbCodec[T] implicit def dbCodecRootJsonCodec[A: RootJsonCodec]: DbCodec[T] implicit def dbCodecBinaryCodec[A: BinaryCodec]: DbCodec[T] case class Square(side: Double) implicit val squareRootJsonCodec = new RootJsonCodec[Square] {...} implicit val squareBinaryCodec = new BinaryCodec[Square] {...} 32
33. ### {HASKELL, RUST} ➡ SCALA ▸ define instances in companion objects

(whenever possible) ▸ define blanket instances in companion objects of target typeclass ▸ be conservative and follow a convention for orphan and blanket instances (more on this later) 33
34. ### OPERATIONS (SYNTAX) in Haskell and Rust, functions defined/imported along with

the typeclass definition -- Haskell area \$ Circle { radius = 5 } // Scala implicitly[HasArea[Square]].area(Square(side = 5)) 34
35. ### IN SCALA, OPERATIONS encoded AS AN IMPLICIT CONVERSION implicit class

hasAreaOps[A: HasArea](a: A) { def area: Float = implicitly[HasArea[A]].area(a) } scala> Square(side = 5).area 35
36. ### SIMULACRUM3 addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) ꔇ import

simulacrum._ @typeclass trait HasArea[A] { def area(self: A) } 3 https://github.com/typelevel/cats 36
37. ### Encoding IN SIMULACRUM @typeclass trait HasArea[A] { def area(self: A):

Double } ‑ trait HasArea[A] { def area(self: A): Double } ꔇ 37
38. ### Syntax IN SIMULACRUM @typeclass trait HasArea[A] { ... } ‑

ꔇ object HasArea { ꔇ trait Ops[A] { def area: Double } trait ToHasAreaOps { implicit def toHasAreaOps[A: HasArea](target: A): Ops[A] = new Ops[A] { <impl> } } ꔇ 38
39. ### Syntax IN SIMULACRUM @typeclass trait HasArea[A] { def area(self: A):

Double } ‑ object HasArea { ꔇ trait AllOps[A] extends Ops[A] { // inherits implementation of def area: Double } object ops { implicit def toAllHasAreaOps[A: HasArea](target: A): AllOps[A] = new AllOps[A] { def area: Double = // ... } } } 39
40. ### Syntax WITHOUT SIMULACRUM trait HasArea[A] { def area(self: A): Double

} object HasArea { object ops { implicit class toHasAreaOps[A: HasArea](target: A) { def area: Double = implicitly[HasArea[A]].area(target) } } } 40
41. ### SYNTAX - usage import HasArea.ops._ scala> Square(side = 5).area or,

only with Simulacrum object GeometryOps extends ToHasAreaOps // and more scala> import GeometryOps._ scala> Square(side = 5).area 41
42. ### SIMULACRUM does not overwrite THE TYPECLASS' COMPANION OBJECT import simulacrum._

@typeclass trait HasArea[A] { def area(self: A): Double } case class Rectangle(a: Double, b: Double) object HasArea { implicit val rectangleHasArea = new HasArea[Rectangle] { def area(self: Rectangle): Double = self.a * self.b } } 42
43. ### WHY SIMULACRUM? ▸ robust, consistent object-oriented forwarders ("syntax") ▸ searchable,

consistent syntax (@typeclass) ▸ A-la-carte imports for syntax (ops) via traits (for usability) 43
44. ### {SIMULACRUM} ➡ SCALA ▸ (discoverability) syntax defined in the companion

object, and re- exported via ops and traits for usability 44
45. ### CATS3 Lightweight, modular, and extensible library for functional programming. Uses

simulacrum. 3 https://github.com/typelevel/cats 45
46. ### CATS: SCOPE AND COHERENCE for cats datatypes, instances deﬁned only

in companion objects for scala datatypes, orphan instances deﬁned only in package cats.instances selective import via traits 46
47. ### {RUST, CATS} ➡ SCALA ▸ prefer companion objects for typeclasses/datatypes

you control ▸ group all orphan instances in a package, allow selective import ▸ avoid overlapping instances, unless there's a specific usability need 47
48. ### NEWTYPE: SAFER ALTERNATIVE TO CONFLICTING INSTANCES (coherence) object HasArea {

implicit val rectangleHasArea = new HasArea[Rectangle] { ... } } ꔇ class Module { case class Spherical[A](shape: A) implicit sphericalRectangleHasArea = new HasArea[Spherical[Rectangle]] { def area(r: Spherical[Rectangle]): Double = // ... } compareArea(Spherical(Rectangle(10, 12)), Spherical(Rectangle(13, 14))) } 48