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

Scala Type classes: What are they useful for?

Scala Type classes: What are they useful for?

Scala Type Classes are often used in different Scala libraries. Type Classes solve problems of OOP polymorphism using elegant ideas of parametric and ad hoc polymorphism. This leads to less coupled and more extensible code when designing a project library or leveraging already designed type system with type classes approach. In order to understand Type Classes, we will shorty look at their origination language, like Haskell, as well as similar idea of Type Classes in Rust. We will go through the Type Class implementation in Scala and look at future support of Type Classes in Scala 3 (Dotty).

Alexey Novakov

August 21, 2019
Tweet

More Decks by Alexey Novakov

Other Decks in Programming

Transcript

  1. Scala [Type] Classes
    What are they useful for?
    BOB Summer 2019, Berlin, Alexey Novakov

    View Slide

  2. About me
    - 4yrs in Scala, 10yrs in Java
    - a fan of Data Processing,
    - Distributed Systems,
    - Functional Programming
    Hi, I am Alexey!
    I am working at Ultra Tendency

    View Slide

  3. Problems to solve
    1. Coupling in polymorphism
    2. Add functionality to an
    already existing
    inheritance chain
    Animal
    Dog Cat
    Pets Wild Animals
    Lion …
    3. Weak Type safety
    (conversions)
    Subtyping

    View Slide

  4. Solution
    Type Classes

    View Slide

  5. • TC solve these problems via ad-hoc
    polymorphism
    • TC in Scala is a programming idiom
    • TC in Scala are based on implicit
    search

    View Slide

  6. View Slide

  7. Origins
    • Borrowed from Haskell
    - ad-hoc polymorphism
    - vs. parametric polymorphism

    View Slide

  8. acts in the same way for each type
    List[T].length

    List[Int].length
    List[Float].length
    parametric polymorphism

    View Slide

  9. acts in different way for each type
    T * T
    * (x: Int, y: Int): Int = …
    * (x: Double, y: Double): Double = …
    ad-hoc polymorphism

    View Slide

  10. Serializable Eq Hashable Printable
    A B C D
    Traits (…Interfaces)
    Classes
    Subtyping (Hierarchical)

    View Slide

  11. Serializable
    Eq
    Hashable …
    Printable …
    [A] [B] [C]
    [D]
    Type Classes
    [D]
    [C]
    [B]
    [A]
    (Linear)

    View Slide

  12. Type Class Implementation

    View Slide

  13. Class with
    generic type a
    Type instance for Int
    Type instance for Float
    Haskell

    View Slide

  14. trait Num[A] {
    def +(l: A, r: A): A
    def *(l: A, r: A): A
    }
    object instances {
    implicit val intNum = new Num[Int] {
    override def +(l: Int, r: Int): Int = l + r
    override def *(l: Int, r: Int): Int = l * r
    }
    implicit val floatNum = new Num[Float] {
    override def +(l: Float, r: Float): Float = l + r
    override def *(l: Float, r: Float): Float = l * r
    }
    }
    object Num {
    def +[A: Num](l: A, r: A): A = implicitly[Num[A]].+(l, r)
    def *[A: Num](l: A, r: A): A = implicitly[Num[A]].*(l, r)
    }
    Class with
    generic type A
    Type instance for Int
    Type instance for Float
    Scala
    Polymorfic functions

    View Slide

  15. Differences
    Haskell
    1. Unique type class instance in global namespace (coherence)
    2. Language support via class and instance keywords
    Scala
    1. Multiple instances can co-exist, but one can be used at a time
    2. No special language support (trait + implicit)

    View Slide

  16. Type Class coherence
    In the context of type
    classes, coherence
    means that there should
    only be one instance of a
    type class for any given
    type

    View Slide

  17. “Multiple instances can co-exist, but one can be
    used at a time”
    From one side: it is flexibility and can be an advantage of Scala
    From another side: this can be error-prone


    You need be to careful in selecting the right instances

    View Slide

  18. Scala type class ingredients

    View Slide

  19. 1. Trait
    with
    [generic]
    params
    2. Implicit
    instance with
    [concrete]
    type
    3. User API
    (set of
    functions)

    View Slide

  20. 1.
    trait Formatter[A] {
    def fmt(a: A): String
    }
    Trait with [generic] params

    View Slide

  21. 2.
    object instances {
    implicit val float: Formatter[Float] =
    (a: Float) => s"$a f"
    implicit val boolean: Formatter[Boolean] =
    (a: Boolean) => a.toString.toUpperCase
    implicit def list[A]
    (implicit ev: Formatter[A]): Formatter[List[A]] =
    (a: List[A]) => a.map(e => ev.fmt(e)).mkString(" :: ")
    }
    Implicit instances with [concrete] types
    Backbone

    View Slide

  22. 3.
    object Formatter {
    def fmt[A](a: A)
    (implicit ev: Formatter[A]): String =
    ev.fmt(a)
    }
    User API (singleton object)
    “ev” short from “evidence”

    View Slide

  23. import instances._
    val floats = List(4.5f, 1f)
    val booleans = List(true, false)
    val integers = List(1, 2, 3)
    println(Formatter.fmt(floats)) // 4.5 f :: 1.0 f
    println(Formatter.fmt(booleans)) // TRUE :: FALSE
    println(Formatter.fmt(integers)) // fail
    // could not find implicit value for parameter
    ev: Formatter[List[Int]]
    In Action

    View Slide

  24. Implicit instance with concrete type
    Placement Search Providers
    - Companion object
    - Any other object or
    trait
    - local or inherited
    definitions
    - import myInstances._
    - Companion obj either of
    TC or Parameter Type
    [A]
    - val
    - def (for recursive
    case)
    2.

    View Slide

  25. 3. User API
    Exposed via:
    - singleton object
    - as extension methods
    (syntactic sugar)
    In form of:
    - Polymorphic functions, which
    take implicit instances

    View Slide

  26. object Formatter {
    // choose this
    def fmt[A](a: A)(implicit ev: Formatter[A])
    : String = ev.fmt(a)
    User API: singleton object
    // or that via context bounds
    def fmt[A : Formatter](a: A) : String =
    Formatter[A].fmt(a)
    //implicitly[Formatter[A]].fmt(a)
    // trick to avoid implicitly method
    def apply[A](implicit ev: Formatter[A])
    : Formatter[A] = ev
    3.
    These two
    is
    popular
    minimum
    set

    View Slide

  27. User API: extension methods
    object syntax {
    implicit class FromatterOps[A: Formatter](a: A) {
    def fmt: String = Formatter[A].fmt(a)
    }
    }

    import syntax._
    println(floats.fmt) // 4.5 f :: 1.0 f
    println(booleans.fmt) // TRUE :: FALSE
    3.

    View Slide

  28. implicit def list[A]
    (implicit ev: Formatter[A]): Formatter[List[A]] =
    (a: List[A]) => a.map(e => ev.fmt(e)).mkString(" :: “)
    Having Formatter[A], we get Formatter[List[A]] almost for free
    Type Classes can be defined inductively
    Observations
    Pattern
    Hypothesis
    Theory
    Inductive reasoning

    View Slide

  29. View Slide

  30. public interface Formatter {
    String fmt(T t);
    }
    class instances {
    static Formatter string = s -> String.format("string: %s", s);
    static Formatter integer = i -> String.format("int: %s", i);
    }
    class Api {
    static String fmt(T t, Formatter ev) {
    return ev.fmt(t);
    }
    }

    public static void main(String[] args) {
    System.out.println(Api.fmt("some string", instances.string));
    System.out.println(Api.fmt(4, instances.integer));
    }

    Java Attempt
    no automatic TC
    instances resolution

    View Slide

  31. When to use Type Classes

    View Slide

  32. • Add new behaviour without modification of the
    existing class or when subtyping is not possible
    EQ Formatter
    MyType
    unwanted/impossible
    implicit val myType: Formatter[MyType]

    View Slide

  33. • TCs allow you to provide evidence that a type outside of
    your "control" conforms with some behaviour. Someone
    else's type can be a member of your typeclass.
    object Formatter {
    def fmt[A](a: A)(implicit ev: Formatter[A]): String =
    ev.fmt(a)
    }
    import instances._
    println(Formatter.fmt(floats)(here comes implicit))

    View Slide

  34. • When dynamic dispatch is unwanted, i.e.
    • subtype polymorphism dispatch is done through the
    runtime type of an object
    • However, Type Classes dispatch on the compile time
    type

    Runtime vs. Compile dispatch

    View Slide

  35. trait Formatter {
    def fmt: String
    }
    case class FloatVal(v: Float) extends Formatter {
    override def fmt: String = s"$v f"
    }
    case class BooleanVal(v: Boolean) extends Formatter {
    override def fmt: String = v.toString.toUpperCase
    }
    val vals: Seq[subtyping.Formatter]
    = List(FloatVal(1), BooleanVal(true), BooleanVal(false))
    vals.foreach(v => printf(v.fmt))
    Subtyping Polymorphism

    View Slide

  36. import instances._
    val floats = List(4.5f, 1f)
    println(
    Formatter
    .fmt[List[Float]](floats)(implicit ev)
    )
    Ad hoc Polymorphism
    Compile time dispatch

    View Slide

  37. Heterogeneous List
    trait W[F[_]] {
    type A
    val a: A
    val fa: F[A]
    }
    object W {
    def apply[F[_], A0](a0: A0)(implicit ev: F[A0]): W[F] =
    new W[F] {
    type A = A0
    val a = a0
    val fa = ev
    }
    }
    val hlist = List(W(1f), W(true), W(2f))
    println(hlist.fmt)
    implicit val e: Formatter[W[Formatter]] =
    (w: W[Formatter]) => w.fa.fmt(w.a)
    Wrapper to
    check
    there is F[A] for
    some A
    Instance
    List(.. W(2)) // Int won’t compile - no instance

    View Slide

  38. Subtyping: pattern matching
    trait Formatter {
    def fmt: String = {
    this match {
    case v: FloatVal => s"${v.v} f"
    case v: BooleanVal =>
    v.v.toString.toUpperCase
    case v: SomeNewType…
    }
    }}
    case class FloatVal(v: Float)
    extends Formatter
    case class BooleanVal(v: Boolean)
    extends Formatter
    Problems:
    1. Every new subtype
    requires a supertype to be
    modified, i.e. leads to
    coupling
    2. Supertype may be
    unavailable for changes

    View Slide

  39. TC as Design Choice
    • Popular design choice for libraries rather than for an application code
    Suppose you have some internal libraries:
    • lib-report
    • lib-core
    • lib-serialization
    • lib-dsl
    ….

    View Slide

  40. Examples
    Cats: Semigroup, Monoid, Applicative, Monad, Traversable
    JSON: Reads, Writes
    Circe: Encoder, Decoder
    uPickle: Reader, Writer
    Scala std library: scala.math.Ordering, scala.math.Numeric
    + other 100500 Scala libraries

    View Slide

  41. • Rust supports traits, which are a limited form of
    type classes with coherence
    • Instances can be also defined inductively
    • Does not support higher-kinded types >

    View Slide

  42. pub trait Formatter {
    fn fmt(&self) -> String;
    }
    impl Formatter for &str {
    fn fmt(&self) -> String {
    "[string: ".to_owned() + &self + "]"
    }
    }
    impl Formatter for i32 {
    fn fmt(&self) -> String {
    "[int_32: ".to_owned() + &self.to_string() + "]"
    }
    }
    impl> Formatter for Vec {
    fn fmt(&self) -> String {
    self.iter().map(|e| e.fmt()).collect::>().join(" :: ")
    }
    }
    fn fmt(t: T) -> String where T: Formatter {
    t.fmt()
    }
    Trait with
    generic type T
    Type instance
    for &str
    Type instance
    for Int
    Type instance
    for Vec
    polymorphic
    function

    View Slide

  43. let x = fmt("Hello, world!”); // or “Hello…”.fmt()
    let i = 4.fmt();
    let ints = vec![1, 2, 3].fmt();
    println!("{}", x);
    //[string: Hello, world!]
    println!("{}", i);
    //[int_32: 4]
    println!("{}", ints)
    //[int_32: 1] :: [int_32: 2] :: [int_32: 3]

    View Slide

  44. let floats = fmt(vec![1.0, 2.0, 3.0]);
    error[E0277]: the trait bound `{float}: Formatter<{float}>` is not satisfied
    --> src/main.rs:31:18
    |
    31 | let floats = fmt(vec![1.0, 2.0, 3.0]);
    | ^^^ the trait `Formatter<{float}>` is not implemented for `{float}`
    |
    = help: the following implementations were found:
    <&str as Formatter<&str>>
    >
    as Formatter>>
    = note: required because of the requirements on the impl of `Formatter>` for `std::vec::Vec<{float}>`
    note: required by `fmt`
    --> src/main.rs:23:1
    |
    23 | fn fmt(t: T) -> String where T: Formatter {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Missing instance for float
    Available instances
    / implementations

    View Slide

  45. Type Classes Generation
    "com.github.mpilquist" %% “simulacrum"
    addCompilerPlugin("org.scalamacros" % "paradise" % “x.y.z”)
    import simulacrum._
    @typeclass trait Formatter[A] {
    def fmt(a: A): String
    }

    View Slide

  46. import Formatter.ops._
    import instances._
    object Main extends App {
    print(true.fmt)
    }

    View Slide

  47. object syntax {
    implicit class FromatterOps[A: Formatter](a: A) {
    def fmt: String = Formatter[A].fmt(a)
    }
    }
    object Formatter {
    def fmt[A: Formatter](a: A): String = Formatter[A].fmt(a)
    def apply[A](implicit formatter: Formatter[A]): Formatter[A] = formatter
    }
    Autogenerated by macro

    View Slide

  48. Current Drawbacks

    View Slide

  49. 1. No language support at the moment
    (encoding using trait, implicit, object)
    trait Formatter[A] { … }
    object instances {
    implicit val …
    implicit val …
    implicit def …
    }
    object syntax {
    implicit class FromatterOps[A: Formatter](a: A) {

    }
    }

    View Slide

  50. 2. Ambiguous instances error may occur when:
    • Multiple Type Class instances for the same type
    • Type Classes hierarchy via subtyping
    Functor
    Traverse Monad
    def myFunc[F[_] : Traverse : Monad] =
    println(implicitly[Functor[F]].map(…))
    Error:(12, 23) ambiguous implicit values:
    both value evidence$2 of type Monad[F]
    and value evidence$1 of type Traverse[F]
    match expected type Functor[F]

    View Slide

  51. 3. Multi-parameter Type Classes (Add[A, B, C])
    trait Add[A, B, C] {
    def +(a: A, b: B): C
    }
    implicit val intAdd1: Add[Int, Int, Double] =
    (a: Int, b: Int) => a + b
    implicit val intAdd2: Add[Int, Int, Int] =
    (a: Int, b: Int) => a + b
    1 + 2 = 3.0
    Instead of 3

    View Slide

  52. Future: Type Classes in Scala 3
    trait Formatter[A] {
    def (a: A)fmt: String
    }
    delegate for Formatter[Float] {
    def (a: Float) fmt: String = s"$a f"
    }
    delegate for Formatter[Boolean] {
    def (a: Boolean) fmt: String = a.toString.toUpperCase
    }
    delegate [T] for Formatter[List[T]] given Formatter[T] {
    def (l: List[T]) fmt: String =
    l.map(e => the[Formatter[T]].fmt(e)).mkString(" :: ")
    }
    println(List(true, false).fmt)
    // TRUE :: FALSE

    View Slide

  53. object Formatter {
    def apply[T] (given Formatter[T]) = the[Formatter[T]]
    def fmt[T: Formatter](a: T): String =
    Formatter[T].fmt(a)
    }
    println(Formatter.fmt(List(true, false)))

    View Slide

  54. Analogous in other languages
    • Agda (instance arguments)
    • Rust (traits)
    • Coq
    • Mercury
    • Clean

    View Slide

  55. Links
    2. Scala Implicit type classes
    http://debasishg.blogspot.com/2010/06/scala-implicits-type-classes-here-i.html
    3. Polymorphism and Typeclasses in Scala
    http://like-a-boss.net/2013/03/29/polymorphism-and-typeclasses-in-scala.html
    https://wiki.haskell.org/OOP_vs_type_classes
    1. Edward Kmett - Type Classes vs. the World:
    https://www.youtube.com/watch?v=hIZxTQP1ifo
    http://tpolecat.github.io/2015/04/29/f-bounds.html
    4. Wiki Haskell: OOP vs. Type classes
    5. Returning the "Current" Type in Scala
    https://www.youtube.com/watch?v=1h8xNBykZqM
    6. Some Mistakes We Made When Designing Implicits – Martin Odersky

    View Slide

  56. Thank you! Questions?
    Alexey Novakov
    email:
    - alexey.novakov at ultratendency.com
    - novakov.alex at gmail.com
    Twitter: @alexey_novakov

    View Slide

  57. https://unsplash.com/photos/jUwvjOmCTWc
    https://unsplash.com/photos/Ow16ATZrs90
    https://unsplash.com/photos/S6wHfOpdGkY
    https://unsplash.com/photos/kcRFW-Hje8Y
    https://whvn.cc/260104
    https://unsplash.com/photos/3y1zF4hIPCg

    View Slide