Skip to content

Instantly share code, notes, and snippets.

@kevinkyyro
Created October 12, 2016 18:04
Show Gist options
  • Save kevinkyyro/ab33df295108c1368cfa8a8c2ce3366b to your computer and use it in GitHub Desktop.
Save kevinkyyro/ab33df295108c1368cfa8a8c2ce3366b to your computer and use it in GitHub Desktop.
A code-based presentation of "Don't Fear the Implicits" by Daniel Westheide
// https://speakerdeck.com/dwestheide/dont-fear-the-implicits-everything-you-need-to-know-about-typeclasses
object `Preview: implicits can be used for...` {
trait Dependencies[T] {
import scala.concurrent.{ExecutionContext, Future}
def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S]
}
trait Configuration {
import akka.util.Timeout
// aka: ask
def ?(message: Any)(implicit timeout: Timeout): Unit
}
trait Context {
import akka.actor.{ActorRef, Actor}
// aka: tell
def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit
import play.api.i18n.Lang
import play.api.libs.json.JsValue
def errorsAsJson(implicit lang: Lang): JsValue
}
}
/*
Compare to:
@Configuration // what's that?
@EnableAuthorizationServer // how?
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // great...
...
}
*/
object `Let's try it!` {
import scala.collection.generic.CanBuildFrom
val xs = Vector(1, 2).map(_ * 2L) // (implicitly[CanBuildFrom[Vector[Int], Long, Vector[Long]]])
val sum = xs.sum // (implicitly[Numeric[Long]])
}
object Motivation {
import org.joda.time.Duration
// A spark thing
trait DistributedDataset[A] {
def foldLeft[B](z: B)(op: (B, A) => B): B
def count: Int
}
object DistributedDataset {
// just a dummy implementation for examples
def distribute[A](as: A*): DistributedDataset[A] = new DistributedDataset[A] {
def foldLeft[B](z: B)(op: (B, A) => B): B = as.foldLeft(z)(op)
def count = as.size
}
}
val durations = DistributedDataset.distribute(
Duration standardMinutes 3,
Duration standardSeconds 17,
Duration standardSeconds 5,
Duration standardHours 1)
}
object `What I want to do` {
import Motivation._
// Let's deal with mass
case class Kilograms(value: BigDecimal) {
def + (y: Kilograms) = Kilograms(value + y.value)
def - (y: Kilograms) = Kilograms(value - y.value)
def * (y: BigDecimal) = Kilograms(value * y)
def / (y: BigDecimal) = Kilograms(value / y)
}
object Kilograms {
val zero = Kilograms(BigDecimal(0))
def sum (xs: DistributedDataset[Kilograms]) = xs.foldLeft(zero)(_ + _)
def mean(xs: DistributedDataset[Kilograms]) = sum(xs) / xs.count
}
// Okay, how about distance?
case class Kilometers(value: BigDecimal) {
def + (y: Kilometers) = Kilometers(value + y.value)
def - (y: Kilometers) = Kilometers(value - y.value)
def * (y: BigDecimal) = Kilometers(value * y)
def / (y: BigDecimal) = Kilometers(value / y)
}
object Kilometers {
val zero = Kilometers(BigDecimal(0))
def sum (xs: DistributedDataset[Kilometers]) = xs.foldLeft(zero)(_ + _)
def mean(xs: DistributedDataset[Kilometers]) = sum(xs) / xs.count
}
}
object `Hmmm... Oh, we can use inheritance, right?` {
import Motivation._
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) = xs.foldLeft(zero)(_ + _)
def mean[A <: Quantity[A]](xs: DistributedDataset[A], zero: A) = sum(xs, zero) / xs.count
}
case class Kilograms(value: BigDecimal) extends Quantity[Kilograms] {
def unit(x: BigDecimal) = Kilograms(x)
}
case class Kilometers(value: BigDecimal) extends Quantity[Kilometers] {
def unit(x: BigDecimal) = Kilometers(x)
}
// Maybe we need should use the adapter pattern
import org.joda.time.Duration
case class Milliseconds(underlying: Duration) extends Quantity[Milliseconds] {
def value = underlying.getMillis
def unit(x: BigDecimal) = Milliseconds(Duration.millis(x.toLong))
}
val durations = DistributedDataset.distribute(
Milliseconds(Duration standardMinutes 3), // lots of allocations
Milliseconds(Duration standardSeconds 17), // boilerplate
Milliseconds(Duration standardSeconds 5), // losing original units
Milliseconds(Duration standardHours 1))
val meanDuration = Quantity.mean(durations, Milliseconds(Duration.ZERO))
}
object `Let's try something different` {
import Motivation._
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)
}
object Quantity {
val kilogramQuantity: Quantity[Kilograms] = new Quantity[Kilograms] {
def value(x: Kilograms) = x.value
def unit (x: BigDecimal) = Kilograms(x)
}
val kilometerQuantity: Quantity[Kilometers] = new Quantity[Kilometers] {
def value(x: Kilometers) = x.value
def unit (x: BigDecimal) = Kilometers(x)
}
val durationQuantity: Quantity[Duration] = new Quantity[Duration] {
override val zero = Duration.ZERO
override def unit (x: BigDecimal) = Duration.millis(x.toLong)
override def value(x: Duration) = BigDecimal(x.getMillis)
override def plus (x: Duration, y: Duration) = x plus y
override def minus(x: Duration, y: Duration) = x minus y
}
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)
}
// let's see how it goes this time
val durations = DistributedDataset.distribute(
Duration standardMinutes 3,
Duration standardSeconds 17,
Duration standardSeconds 5,
Duration standardHours 1)
val meanDuration = Quantity.mean(durations, Quantity.durationQuantity)
trait `How we get from here to implicits` {
// original
def sum1 [A](xs: DistributedDataset[A], quantity: Quantity[A]): A
def mean1[A](xs: DistributedDataset[A], quantity: Quantity[A]): A
// usage:
// sum1(durations, Quantity.durationQuantity)
// separate parameter lists
def sum2 [A](xs: DistributedDataset[A])(quantity: Quantity[A]): A
def mean2[A](xs: DistributedDataset[A])(quantity: Quantity[A]): A
// usage:
// sum2(durations)(Quantity.durationQuantity)
// implicit parameter list
def sum3 [A](xs: DistributedDataset[A])(implicit quantity: Quantity[A]): A
def mean3[A](xs: DistributedDataset[A])(implicit quantity: Quantity[A]): A
// usage:
// implicit val durationQuantity = Quantity.durationQuantity
// sum3(durations)
}
}
object `Okay, what now?` {
import Motivation._
import org.joda.time.Duration
// let's re-write
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)
}
object Quantity {
implicit val kilogramQuantity: Quantity[Kilograms] = new Quantity[Kilograms] {
def value(x: Kilograms) = x.value
def unit (x: BigDecimal) = Kilograms(x)
}
implicit val kilometerQuantity: Quantity[Kilometers] = new Quantity[Kilometers] {
def value(x: Kilometers) = x.value
def unit (x: BigDecimal) = Kilometers(x)
}
implicit val durationQuantity: Quantity[Duration] = new Quantity[Duration] {
override val zero = Duration.ZERO
override def unit (x: BigDecimal) = Duration.millis(x.toLong)
override def value(x: Duration) = BigDecimal(x.getMillis)
override def plus (x: Duration, y: Duration) = x plus y
override def minus(x: Duration, y: Duration) = x minus y
}
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)
}
val meanDuration1 = Quantity.mean(durations)(Quantity.durationQuantity)
val meanDuration2 = Quantity.mean(durations) // compiler fills in the implicit for us
// welcome to typeclasses!
// syntax: context-bounds
def sum[A: Quantity](xs: DistributedDataset[A]): A = {
val quantity = implicitly[Quantity[A]]
xs.foldLeft(quantity.zero)(quantity.plus)
}
/**
* Implicit resolution rules:
*
* Explicit:
* ```
* val fooQuant = new Quantity[Foo] { ... }
* sum(myFoos)(fooQuant)
* ```
*
* Local:
* ```
* implicit val fooQuant = new Quantity[Foo] { ... }
* sum(myFoos) /*(fooQuant)*/
* ```
*
*/
// Implicit resolution rules:
trait `Implicit resolution rules` {
class Foo
val foos: DistributedDataset[Foo] = ???
//
// explicit scope, e.g. the symbol itself is visible
//
// #1
def explicit = {
val fooQuant: Quantity[Foo] = ???
sum(foos)(fooQuant)
}
// #2
def local = {
implicit val fooQuant: Quantity[Foo] = ???
sum(foos)
}
// #3
def imported = {
object SomeObj {
object Bar {
implicit val fooQuant: Quantity[Foo] = ???
}
}
import SomeObj.Bar.fooQuant
sum(foos) // SomeObj.Bar.fooQuant
}
trait Bar {
implicit val fooQuant: Quantity[Foo] = ???
}
// #4
class InheritFromBar extends Bar {
sum(foos) // (super.fooQuant)
}
// super.fooQuant is not visibile here
// #5
def packageObject = {
// ... package objects are a thing, hard to show in a worksheet ...
}
//
// implicit scope, e.g. symbol not visible, but can be resolved implicitly
//
// #6
def companionOfTypeclass = ???
// #7 - for Quantity[A], the companion of A
def companionOfTypeParameter = ???
// #8 - probably the companion of the super type of A for Quantity[A]
def companionOfSuperTypes = {
// trait A
// object A {
// implicit def quantA: Quantity[A] = ???
// }
//
// class B extends A
//
// implicitly[Quantity[B]]
}
}
}
// This allows us to prioritizing our implicits
object Prioritizing {
import org.joda.time.Duration
trait Show[A] { def show(a: A): String }
trait LowPriorityImplicits {
implicit def defaultInstance[A]: Show[A] = new Show[A] {
def show(a: A) = a.toString
}
}
object Show extends LowPriorityImplicits {
def apply[A: Show](a: A) = implicitly[Show[A]].show(a)
implicit val showDurationPoorly: Show[Duration] = new Show[Duration] {
def show(d: Duration) = s"${d.getStandardHours}:${d.getStandardMinutes}"
}
}
case class Foo(age: Int)
object Foo {
implicit val showFoo: Show[Foo] = ???
}
Show.apply(Duration.millis(60 * 60 * 1000)) // showDurationPoorly
Show(Option(42)) // LowPriorityImplicits#defaultInstance
def takeControl = {
import Show.defaultInstance // \m/
Show(Duration.millis(60 * 60 * 1000))
}
}
object `But OOP Syntax!` {
trait Plus[A] { def plus(x: A, y: A): A }
object Plus {
implicit class Syntax[A](x: A)(implicit addA: Plus[A]) {
def +(y: A) = addA.plus(x, y)
}
}
import Plus.Syntax
case class Age(years: Int)
implicit val plusInt = new Plus[Age] { def plus(x: Age, y: Age) = Age(x.years + y.years) }
Age(20) + Age(22) // why would we add ages? no clue, but we did it
}
@kevinkyyro
Copy link
Author

class A
class B

sealed abstract class AorB[T]
object AorB {
  implicit object witnessA extends AorB[A]
  implicit object witnessB extends AorB[B]

  // derivation
  implicit def witnessListOfAorB[T: AorB] = new AorB[List[T]] {}
}

// T must be (A | B | List[A] | List[B])
def foo[T](t: T)(implicit witness: AorB[T]) = ???

foo(new A)
foo(new B)
foo(List.empty[A])
//foo(42) // doesn't compile! 

@kevinkyyro
Copy link
Author

kevinkyyro commented Oct 12, 2016

Some (maybe) final notes about what implicits can do/are well suited for:

  • conversions: if something doesn't type-check, the compiler will see if an implicit def in scope applied to the expression will make it compile
    • warning: this can easily be abused pretty horribly, so generally is only used for next point...
  • post-hoc extension of existing, uncontrolled types (e.g. from a library)
    • equivalent to separately defining class and implicit conversion separately
    • usage: (new FooFromSomeLibrary).someNewMethod
    • definition:
    implicit class FooExtension(foo: FooFromSomeLibrary) { 
      def someNewMethod = ???
    }
  • derive other implicits (e.g. take a typeclass for A and return a typeclass for List[A])
    // probably doesn't have a reasonable definition, so don't actually do it for quantities
    implicit def listQuant[A: Quantity] = new Quantity[List[A]] {...} 
  • type constraints (ex: union types, type equivalence, etc)
    // note: arity-2 types can be written infix, like A =:= Int (good) or Person Map List[Pet] (weird)
    sealed abstract class =:=[A, B]
    object =:= {
      implicit def sameType[A]: =:=[A, A] = new =:=[A, A] {} // only way to create a SameType
    }

    class Foo[A] {
      def somethingThatOnlyWorksOnInts(implicit AisInt: A =:= Int) = ???
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment