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

April 25, 2019
Tweet

More Decks by Alexey Novakov

Other Decks in Programming

Transcript

  1. Scala Type Classes: What are they useful for? !1 A

    l e x e y N o v a k o v i n n o Q O f f e n b a c h , S c a l a M e e t u p 2 5 . 0 4 . 2 0 1 9
  2. - 4yrs in Scala, 10yrs in Java - a fan

    of Data Processing, - Distributed Systems, - Functional Programming ALEXEY NOVAKOV Senior Consultant bei INNOQ Deutschland GmbH
  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 Subtyping
  4. • TC solve these problems via ad-hoc polymorphism • TC

    in Scala is a programming idiom • TC in Scala are based on implicit search
  5. Function is defined for a range of types, but act

    in the same way for each type List[T].length “parametric polymorphism”
  6. Function is defined for a range of types, but acting

    in different way for each type T * T “ad-hoc polymorphism”
  7. Serializable Eq Hashable … Printable … [A] [B] [C] [D]

    Type Classes [D] [C] [B] [A] (Linear)
  8. 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
  9. 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)
  10. 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
  11. “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
  12. 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
  13. 3. object Formatter { def fmt[A](a: A) (implicit ev: Formatter[A]):

    String = ev.fmt(a) } User API (singleton object) “ev” short from “evidence”
  14. 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
  15. 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.
  16. 3. User API Exposed via: - singleton object - as

    extension methods (syntactic sugar) In form of: - Polymorphic functions, which take implicit instances
  17. 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
  18. 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.
  19. 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
  20. 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
  21. • 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]
  22. • 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))
  23. • 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
  24. 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
  25. 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
  26. 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
  27. 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 ….
  28. 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
  29. • 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>>
  30. 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
  31. 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]
  32. 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
  33. 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 }
  34. 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
  35. 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) { … } }
  36. 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]]) 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]
  37. 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
  38. Future: Type Classes in Scala 3 trait Formatter[A] { def

    (a: A)fmt: String } implied for Formatter[Float] { def (a: Float) fmt: String = s"$a f" } implied for Formatter[Boolean] { def (a: Boolean) fmt: String = a.toString.toUpperCase } implied [T] given (ev: Formatter[T]) for Formatter[List[T]] { def (l: List[T]) fmt: String = l.map(e => the[Formatter[T]].fmt(e)).mkString(" :: ") } println(List(true, false).fmt) // TRUE :: FALSE
  39. 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)))
  40. 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
  41. Krischerstr. 100 40789 Monheim am Rhein Germany +49 2173 3366-0

    Ohlauer Str. 43 10999 Berlin Germany +49 2173 3366-0 Ludwigstr. 180E 63067 Offenbach Germany +49 2173 3366-0 Kreuzstr. 16 80331 München Germany +49 2173 3366-0 Hermannstrasse 13 20095 Hamburg Germany +49 2173 3366-0 Gewerbestr. 11 CH-6330 Cham Switzerland +41 41 743 0116 innoQ Deutschland GmbH innoQ Schweiz GmbH www.innoq.com 56 Thank you! Questions? Alexey Novakov [email protected] @alexey_novakov