Slide 1

Slide 1 text

Typeclasses from the Ground Up Daniel Westheide, 28 September 2016

Slide 2

Slide 2 text

About me > consultant at innoQ Germany > author of The Neophyte’s Guide to Scala > Twitter: @kaffeecoder > Website: danielwestheide.com

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

val xs = Vector(1, 2).map(_ * 2) val sum = xs.sum CanBuildFrom Numeric

Slide 5

Slide 5 text

Example > given: big data framework for processing distributed data sets > need: processing of various types of quantities

Slide 6

Slide 6 text

DistributedDataset trait DistributedDataset[A] { def foldLeft[B](z: B)(op: (B, A) => B): B def count: Int }

Slide 7

Slide 7 text

case class Kilograms(value: BigDecimal) { def +(y: Kilograms): Kilograms = Kilograms(value + y.value) def -(y: Kilograms): Kilograms = Kilograms(value - y.value) def *(y: BigDecimal): Kilograms = Kilograms(value * y) def /(y: BigDecimal): Kilograms = Kilograms(value / y) } object Kilograms { val zero: Kilograms = Kilograms(BigDecimal(0)) def sum(xs: DistributedDataset[Kilograms]): Kilograms = xs.foldLeft(zero)(_ + _) def mean(xs: DistributedDataset[Kilograms]): Kilograms = sum(xs) / xs.count }

Slide 8

Slide 8 text

case class Kilometers(value: BigDecimal) { def +(y: Kilometers): Kilometers = Kilometers(value + y.value) def -(y: Kilometers): Kilometers = Kilometers(value - y.value) def *(y: BigDecimal): Kilometers = Kilometers(value * y) def /(y: BigDecimal): Kilometers = Kilometers(value / y) } object Kilometers { val zero: Kilometers = Kilometers(BigDecimal(0)) def sum(xs: DistributedDataset[Kilometers]): Kilometers = xs.foldLeft(zero)(_ + _) def mean(xs: DistributedDataset[Kilometers]): Kilometers = sum(xs) / xs.count }

Slide 9

Slide 9 text

I know! Let’s use inheritance! paris rodin thinker (c) Mark B. Schlemmer https://www.flickr.com/photos/mbschlemmer/3122967980 CC-BY 2.0

Slide 10

Slide 10 text

trait Quantity[A <: Quantity[A]] { def value: BigDecimal def unit(x: BigDecimal): A def +(y: A): A = unit(value + y.value) def -(y: A): A = unit(value - y.value) def *(y: BigDecimal): A = unit(value * y) def /(y: BigDecimal): A = unit(value / y) } object Quantity { def sum[A <: Quantity[A]](xs: DistributedDataset[A], zero: A): A = xs.foldLeft(zero)(_ + _) def mean[A <: Quantity[A]](xs: DistributedDataset[A], zero: A): A = sum(xs, zero) / xs.count }

Slide 11

Slide 11 text

case class Kilograms(value: BigDecimal) extends Quantity[Kilograms] { override def unit(x: BigDecimal): Kilograms = Kilograms(x) } case class Kilometers(value: BigDecimal) extends Quantity[Kilometers] { override def unit(x: BigDecimal): Kilometers = Kilometers(x) }

Slide 12

Slide 12 text

Maybe I should use an adapter… USB ethernet adapter (c) Ash Kyd https://www.flickr.com/photos/ashkyd/14359608157 CC-BY 2.0

Slide 13

Slide 13 text

import org.joda.time.Duration /* Adapter pattern */ case class Milliseconds(underlying: Duration) extends Quantity[Milliseconds] { override def value: BigDecimal = underlying.getMillis override def unit(x: BigDecimal): Milliseconds = Milliseconds(Duration.millis(x.toLong)) }

Slide 14

Slide 14 text

val durations: DistributedDataset[Milliseconds] = DistributedDataset.distribute(Seq( Milliseconds(Duration.standardMinutes(3)), Milliseconds(Duration.standardSeconds(17)), Milliseconds(Duration.standardSeconds(5)), Milliseconds(Duration.standardHours(1)))) val meanDuration = Quantity.mean(durations, Milliseconds(Duration.ZERO)) Lots of new objects boilerplate losing the original type

Slide 15

Slide 15 text

import org.joda.time.Duration case class Kilograms(value: BigDecimal) case class Kilometers(value: BigDecimal) trait Quantity[A] { def value(x: A): BigDecimal def unit(x: BigDecimal): A def zero: A = unit(BigDecimal(0)) def plus(x: A, y: A): A = unit(value(x) + value(y)) def minus(x: A, y: A): A = unit(value(x) - value(y)) def times(x: A, y: BigDecimal): A = unit(value(x) * y) def div(x: A, y: BigDecimal): A = unit(value(x) / y) }

Slide 16

Slide 16 text

import org.joda.time.Duration object Quantity { val kilogramQuantity: Quantity[Kilograms] = new Quantity[Kilograms] { override def value(x: Kilograms): BigDecimal = x.value override def unit(x: BigDecimal): Kilograms = Kilograms(x) } val kilometerQuantity: Quantity[Kilometers] = new Quantity[Kilometers] { override def value(x: Kilometers): BigDecimal = x.value override def unit(x: BigDecimal): Kilometers = Kilometers(x) } // to be continued }

Slide 17

Slide 17 text

import org.joda.time.Duration object Quantity { // continued val durationQuantity: Quantity[Duration] = new Quantity[Duration] { override val zero: Duration = Duration.ZERO override def value(x: Duration): BigDecimal = x.getMillis override def plus(x: Duration, y: Duration): Duration = x.plus(y) override def minus(x: Duration, y: Duration): Duration = x.minus(y) override def unit(x: BigDecimal): Duration = Duration.millis(x.toLong) } // to be continued }

Slide 18

Slide 18 text

import org.joda.time.Duration object Quantity { // continued def sum[A](xs: DistributedDataset[A], quantity: Quantity[A]): A = xs.foldLeft(quantity.zero)(quantity.plus) def mean[A](xs: DistributedDataset[A], quantity: Quantity[A]): A = quantity.div(sum(xs, quantity), xs.count) }

Slide 19

Slide 19 text

val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations, Quantity.durationQuantity)

Slide 20

Slide 20 text

def sum[A](xs: DistributedDataset[A], quantity: Quantity[A]): A = xs.foldLeft(quantity.zero)(quantity.plus) def mean[A](xs: DistributedDataset[A], quantity: Quantity[A]): A = quantity.div(sum(xs, quantity), xs.count)

Slide 21

Slide 21 text

def sum[A](xs: DistributedDataset[A])(quantity: Quantity[A]): A = xs.foldLeft(quantity.zero)(quantity.plus) def mean[A](xs: DistributedDataset[A])(quantity: Quantity[A]): A = quantity.div(sum(xs)(quantity), xs.count) separate parameter list separate parameter list

Slide 22

Slide 22 text

import org.joda.time.Duration val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations, Quantity.durationQuantity)

Slide 23

Slide 23 text

import org.joda.time.Duration val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations)(Quantity.durationQuantity) separate parameter list

Slide 24

Slide 24 text

def sum[A](xs: DistributedDataset[A]) (implicit quantity: Quantity[A]): A = xs.foldLeft(quantity.zero)(quantity.plus) def mean[A](xs: DistributedDataset[A]) (implicit quantity: Quantity[A]): A = quantity.div(sum(xs), xs.count)

Slide 25

Slide 25 text

import org.joda.time.Duration object Quantity { implicit val kilogramQuantity: Quantity[Kilograms] = ??? implicit val kilometerQuantity: Quantity[Kilometers] = ??? implicit val durationQuantity: Quantity[Duration] = ??? }

Slide 26

Slide 26 text

import org.joda.time.Duration val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations)(Quantity.durationQuantity)

Slide 27

Slide 27 text

import org.joda.time.Duration val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations) implicits filled in by compiler

Slide 28

Slide 28 text

And this is how we discovered typeclasses…

Slide 29

Slide 29 text

Context bounds def sum[A: Quantity](xs: DistributedDataset[A]): A = { val quantity = implicitly[Quantity[A]] xs.foldLeft(quantity.zero)(quantity.plus) } def mean[A: Quantity](xs: DistributedDataset[A]): A = { val quantity = implicitly[Quantity[A]] quantity.div(sum(xs), xs.count) }

Slide 30

Slide 30 text

Implicit resolution Imported Inherited in package object Local Explicit Implicit scope > companion object of typeclass > companion object of A > companion objects of super types

Slide 31

Slide 31 text

Local implicit import org.joda.time.Duration implicit val durationQuantity: Quantity[Duration] = ??? val durations: DistributedDataset[Duration] = DistributedDataset.distribute(Seq( Duration.standardMinutes(3), Duration.standardSeconds(17), Duration.standardSeconds(5), Duration.standardHours(1))) val meanDuration = Quantity.mean(durations)

Slide 32

Slide 32 text

Imported implicits import org.joda.time.Duration trait LowPriorityImplicits { implicit val durationQuantity: Quantity[Duration] = ??? } object Implicits extends LowPriorityImplicits

Slide 33

Slide 33 text

Imported implicits import org.joda.time.Duration import Implicits._ val durations: DistributedDataset[Duration] = ??? val meanDuration = Quantity.mean(durations) imported implicit Quantity[Duration]

Slide 34

Slide 34 text

Inherited implicits import org.joda.time.Duration object MixinExample extends App with LowPriorityImplicits { val durations: DistributedDataset[Duration] = ??? val meanDuration = Quantity.mean(durations) println(meanDuration) } mixed in implicit Quantity[Duration]

Slide 35

Slide 35 text

Overriding implicits import org.joda.time.Duration object OverridingExample extends App with LowPriorityImplicits { implicit val myDurationQuantity: Quantity[Duration] = ??? val durations: DistributedDataset[Duration] = ??? val meanDuration = Quantity.mean(durations) println(meanDuration) }

Slide 36

Slide 36 text

Operators package quantities package object lib { implicit class QuantityOps[A](a: A)(implicit quantity: Quantity[A]) { def +(a2: A): A = quantity.plus(a, a2) def -(a2: A): A = quantity.minus(a, a2) def *(y: BigDecimal): A = quantity.times(a, y) def /(y: BigDecimal): A = quantity.div(a, y) } }

Slide 37

Slide 37 text

Operators import quantities.v10.lib._ val nineKilos = Kilograms(3) * 3 val twelveKilos = nineKilos + Kilograms(3)

Slide 38

Slide 38 text

Usability

Slide 39

Slide 39 text

Shortcut for implicitly object Quantity { def apply[A](implicit quantity: Quantity[A]): Quantity[A] = quantity def sum[A : Quantity](xs: DistributedDataset[A]): A = xs.foldLeft(Quantity[A].zero)(Quantity[A].plus) def mean[A : Quantity](xs: DistributedDataset[A]): A = Quantity[A].div(sum(xs), xs.count) }

Slide 40

Slide 40 text

Typeclass constructors object Quantity { def simple[A](unitF: BigDecimal => A) (valueF: A => BigDecimal): Quantity[A] = new Quantity[A] { override def value(x: A): BigDecimal = valueF(x) override def unit(x: BigDecimal): A = unitF(x) } implicit val kilogramQuantity = Quantity.simple(Kilograms)(_.value) implicit val kilometerQuantity = Quantity.simple(Kilometers)(_.value) }

Slide 41

Slide 41 text

Combinators

Slide 42

Slide 42 text

Combinators: Circe import io.circe.{Encoder, Json} import io.circe.Encoder._ case class BookId(value: Int) extends AnyVal object BookId { implicit val bookIdEncoder: Encoder[BookId] = Encoder[Int].contramap(_.value) } package io.circe trait Encoder[A] extends Serializable { self => def apply(a: A): Json final def contramap[B](f: B => A): Encoder[B] = new Encoder[B] { final def apply(a: B) = self(f(a)) } }

Slide 43

Slide 43 text

Generic typeclass instances

Slide 44

Slide 44 text

Generic instances: Circe package io.circe object Encoder { implicit final def encodeOption[A](implicit e: Encoder[A]): Encoder[Option[A]] = new Encoder[Option[A]] { final def apply(a: Option[A]): Json = a match { case Some(v) => e(v) case None => Json.Null } } }

Slide 45

Slide 45 text

Default instances > Provide default instances that make sense most of the time > Make it easy to opt out of default instances

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Questions to ask > Is retroactive extension an important use case? > Can you provide good defaults? > How much boilerplate do you avoid? How much clarity do you lose? > Can you support two usage models, one of them not relying on implicits?

Slide 48

Slide 48 text

Summary > Typeclasses enable retroactive extension > Alternative to inheritance and adapters > Provide simple constructors and combinators > Powerful due to automatic derivation > Don’t throw typeclasses at every problem

Slide 49

Slide 49 text

Thank you for your attention! Twitter: @kaffeecoder Website: danielwestheide.com