Typeclasses from the Ground Up

Typeclasses from the Ground Up

When it comes to typeclasses and implicits – the mechanism used for implementing this pattern in Scala – there appears to be a big gulf: They are widely used not only in many popular Scala libraries, but also in the standard library. On the other hand, many Scala developers are not familiar with them or find them too daunting.
Since it is difficult to evade them when writing real world Scala code, I would like to encourage developers adopting Scala to overcome their fear of implicits and instead embrace the typeclass pattern. At the same time, I want to raise awareness of the problems that can arise when you do so too eagerly.
In this talk, as an intermediate Scala developer, you will learn typeclasses from the ground up: What they are good for and how they compare to what you are familiar with from object-oriented languages, when you should and should not use them, how the pattern can be encoded in Scala and how to write your own typeclasses, how to provide instances of typeclasses for your own or existing types, and how to do all of this with minimal boilerplate. Throughout the talk, you will see numerous examples of typeclasses used in the Scala ecosystem and the standard library, and you’ll see that you don’t need to know anything about category theory to benefit from embracing type classes.

7abf07f13ed689874500c08bc7fbd543?s=128

Daniel Westheide

September 28, 2016
Tweet

Transcript

  1. Typeclasses from the Ground Up Daniel Westheide, 28 September 2016

  2. About me > consultant at innoQ Germany > author of

    The Neophyte’s Guide to Scala > Twitter: @kaffeecoder > Website: danielwestheide.com
  3. None
  4. val xs = Vector(1, 2).map(_ * 2) val sum =

    xs.sum CanBuildFrom Numeric
  5. Example > given: big data framework for processing distributed data

    sets > need: processing of various types of quantities
  6. DistributedDataset trait DistributedDataset[A] { def foldLeft[B](z: B)(op: (B, A) =>

    B): B def count: Int }
  7. 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 }
  8. 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 }
  9. I know! Let’s use inheritance! paris rodin thinker (c) Mark

    B. Schlemmer https://www.flickr.com/photos/mbschlemmer/3122967980 CC-BY 2.0
  10. 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 }
  11. 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) }
  12. Maybe I should use an adapter… USB ethernet adapter (c)

    Ash Kyd https://www.flickr.com/photos/ashkyd/14359608157 CC-BY 2.0
  13. 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)) }
  14. 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
  15. 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) }
  16. 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 }
  17. 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 }
  18. 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) }
  19. 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)
  20. 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)
  21. 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
  22. 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)
  23. 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
  24. 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)
  25. import org.joda.time.Duration object Quantity { implicit val kilogramQuantity: Quantity[Kilograms] =

    ??? implicit val kilometerQuantity: Quantity[Kilometers] = ??? implicit val durationQuantity: Quantity[Duration] = ??? }
  26. 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)
  27. 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
  28. And this is how we discovered typeclasses…

  29. 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) }
  30. Implicit resolution Imported Inherited in package object Local Explicit Implicit

    scope > companion object of typeclass > companion object of A > companion objects of super types
  31. 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)
  32. Imported implicits import org.joda.time.Duration trait LowPriorityImplicits { implicit val durationQuantity:

    Quantity[Duration] = ??? } object Implicits extends LowPriorityImplicits
  33. Imported implicits import org.joda.time.Duration import Implicits._ val durations: DistributedDataset[Duration] =

    ??? val meanDuration = Quantity.mean(durations) imported implicit Quantity[Duration]
  34. 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]
  35. 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) }
  36. 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) } }
  37. Operators import quantities.v10.lib._ val nineKilos = Kilograms(3) * 3 val

    twelveKilos = nineKilos + Kilograms(3)
  38. Usability

  39. 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) }
  40. 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) }
  41. Combinators

  42. 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)) } }
  43. Generic typeclass instances

  44. 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 } } }
  45. Default instances > Provide default instances that make sense most

    of the time > Make it easy to opt out of default instances
  46. None
  47. 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?
  48. 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
  49. Thank you for your attention! Twitter: @kaffeecoder Website: danielwestheide.com