Share
Embed
Typeclasses — a type system construct
Presented at Scala, Warsaw - http://scalar-conf.com
Andrea Lattuada
April 07, 2017
Transcript
50 slides
-
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 -
1. why typeclasses 2. as first-class citizens, encoded 3. complications
arising from encoding 4. case studies, good practices 2 -
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 -
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 -
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 -
// 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 -
1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO
THE "GROUP" THAT SUPPORTS THESE OPERATIONS 8 -
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 -
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 -
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 -
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 -
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 -
1. (TYPECLASS) DEFINE SUPPORTED OPERATIONS 2. (INSTANCES) ADD TYPES TO
THE "GROUP" THAT SUPPORTS THESE OPERATIONS 15 -
1. TYPECLASS definition IN SCALA -- Haskell class HasArea a
where area :: a -> Float ‑ // Scala trait HasArea[A] { def area(x: A): Float } 16 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
SCALA - INSTANCE SCOPE based on implicit resolution ▸ most
specific first ▸ search in implicit scope and companion objects 26 -
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 -
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 -
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 -
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 -
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 -
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 -
{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 -
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 -
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 -
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 -
Encoding IN SIMULACRUM @typeclass trait HasArea[A] { def area(self: A):
Double } ‑ trait HasArea[A] { def area(self: A): Double } ꔇ 37 -
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 -
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 -
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 -
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 -
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 -
WHY SIMULACRUM? ▸ robust, consistent object-oriented forwarders ("syntax") ▸ searchable,
consistent syntax (@typeclass) ▸ A-la-carte imports for syntax (ops) via traits (for usability) 43 -
{SIMULACRUM} ➡ SCALA ▸ (discoverability) syntax defined in the companion
object, and re- exported via ops and traits for usability 44 -
CATS3 Lightweight, modular, and extensible library for functional programming. Uses
simulacrum. 3 https://github.com/typelevel/cats 45 -
CATS: SCOPE AND COHERENCE for cats datatypes, instances defined only
in companion objects for scala datatypes, orphan instances defined only in package cats.instances selective import via traits 46 -
{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 -
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