130

# Typeclasses — a type system construct

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

April 07, 2017

## Transcript

1. 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. 2.

### 1. why typeclasses 2. as first-class citizens, encoded 3. complications

arising from encoding 4. case studies, good practices 2
3. 3.

4. 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. 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. 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. 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. 8.

### 1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO

THE "GROUP" THAT SUPPORTS THESE OPERATIONS 8
9. 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. 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. 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. 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
13. 13.

14. 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. 15.

### 1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO

THE "GROUP" THAT SUPPORTS THESE OPERATIONS 15
16. 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. 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. 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. 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. 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
21. 21.

22. 22.

23. 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. 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. 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. 26.

### SCALA - INSTANCE SCOPE based on implicit resolution ▸ most

specific first ▸ search in implicit scope and companion objects 26
27. 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. 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. 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. 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. 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. 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. 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. 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. 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. 36.

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

### Encoding IN SIMULACRUM @typeclass trait HasArea[A] { def area(self: A):

Double } ‑ trait HasArea[A] { def area(self: A): Double } ꔇ 37
38. 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. 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. 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. 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. 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. 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. 44.

### {SIMULACRUM} ➡ SCALA ▸ (discoverability) syntax defined in the companion

object, and re- exported via ops and traits for usability 44
45. 45.

### CATS3 Lightweight, modular, and extensible library for functional programming. Uses

simulacrum. 3 https://github.com/typelevel/cats 45
46. 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. 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. 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
49. 49.

50. 50.