Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Daniel Westheide

September 28, 2016
Tweet

More Decks by Daniel Westheide

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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

    View Slide

  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
    }

    View Slide

  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)
    }

    View Slide

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

    View Slide

  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))
    }

    View Slide

  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

    View Slide

  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)
    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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)
    }

    View Slide

  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)

    View Slide

  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)

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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)

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  28. And this is how we
    discovered typeclasses…

    View Slide

  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)
    }

    View Slide

  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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  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]

    View Slide

  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)
    }

    View Slide

  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)
    }
    }

    View Slide

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

    View Slide

  38. Usability

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  41. Combinators

    View Slide

  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))
    }
    }

    View Slide

  43. Generic typeclass instances

    View Slide

  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
    }
    }
    }

    View Slide

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

    View Slide

  46. View Slide

  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?

    View Slide

  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

    View Slide

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

    View Slide