Created
October 12, 2016 18:04
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some (maybe) final notes about what
implicit
s can do/are well suited for:implicit def
in scope applied to the expression will make it compileclass
andimplicit
conversion separately(new FooFromSomeLibrary).someNewMethod
implicit
s (e.g. take a typeclass forA
and return a typeclass forList[A]
)