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

7a04b88e1469561db6da3818348d4b8f?s=128

Alexey Novakov

August 21, 2019
Tweet

Transcript

  1. Scala [Type] Classes What are they useful for? BOB Summer

    2019, Berlin, Alexey Novakov
  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
  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
  4. Solution Type Classes

  5. • TC solve these problems via ad-hoc polymorphism • TC

    in Scala is a programming idiom • TC in Scala are based on implicit search
  6. None
  7. Origins • Borrowed from Haskell - ad-hoc polymorphism - vs.

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

    List[Int].length List[Float].length parametric polymorphism
  9. acts in different way for each type T * T

    * (x: Int, y: Int): Int = … * (x: Double, y: Double): Double = … ad-hoc polymorphism
  10. Serializable Eq Hashable Printable A B C D Traits (…Interfaces)

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

    Type Classes [D] [C] [B] [A] (Linear)
  12. Type Class Implementation

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

    instance for Float Haskell
  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
  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)
  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
  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
  18. Scala type class ingredients

  19. 1. Trait with [generic] params 2. Implicit instance with [concrete]

    type 3. User API (set of functions)
  20. 1. trait Formatter[A] { def fmt(a: A): String } Trait

    with [generic] params
  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
  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”
  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
  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.
  25. 3. User API Exposed via: - singleton object - as

    extension methods (syntactic sugar) In form of: - Polymorphic functions, which take implicit instances
  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
  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.
  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
  29. None
  30. public interface Formatter<T> { String fmt(T t); } class instances

    { static Formatter<String> string = s -> String.format("string: %s", s); static Formatter<Integer> integer = i -> String.format("int: %s", i); } class Api { static <T> String fmt(T t, Formatter<T> 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
  31. When to use Type Classes

  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]
  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))
  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
  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
  36. import instances._ val floats = List(4.5f, 1f) println( Formatter .fmt[List[Float]](floats)(implicit

    ev) ) Ad hoc Polymorphism Compile time dispatch
  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
  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
  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 ….
  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
  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 <T<U>>
  42. pub trait Formatter<T> { fn fmt(&self) -> String; } impl

    Formatter<Self> for &str { fn fmt(&self) -> String { "[string: ".to_owned() + &self + "]" } } impl Formatter<Self> for i32 { fn fmt(&self) -> String { "[int_32: ".to_owned() + &self.to_string() + "]" } } impl<T: Formatter<T>> Formatter<Self> for Vec<T> { fn fmt(&self) -> String { self.iter().map(|e| e.fmt()).collect::<Vec<_>>().join(" :: ") } } fn fmt<T>(t: T) -> String where T: Formatter<T> { t.fmt() } Trait with generic type T Type instance for &str Type instance for Int Type instance for Vec<T> polymorphic function
  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]
  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>> <i32 as Formatter<i32>> <std::vec::Vec<T> as Formatter<std::vec::Vec<T>>> = note: required because of the requirements on the impl of `Formatter<std::vec::Vec<{float}>>` for `std::vec::Vec<{float}>` note: required by `fmt` --> src/main.rs:23:1 | 23 | fn fmt<T>(t: T) -> String where T: Formatter<T> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing instance for float Available instances / implementations
  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 }
  46. import Formatter.ops._ import instances._ object Main extends App { print(true.fmt)

    }
  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
  48. Current Drawbacks

  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) { … } }
  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]
  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
  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
  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)))
  54. Analogous in other languages • Agda (instance arguments) • Rust

    (traits) • Coq • Mercury • Clean
  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
  56. Thank you! Questions? Alexey Novakov email: - alexey.novakov at ultratendency.com

    - novakov.alex at gmail.com Twitter: @alexey_novakov
  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