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. 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
  2. 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
  3. • TC solve these problems via ad-hoc polymorphism • TC

    in Scala is a programming idiom • TC in Scala are based on implicit search
  4. acts in the same way for each type List[T].length …

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

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

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

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

    extension methods (syntactic sugar) In form of: - Polymorphic functions, which take implicit instances
  16. 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
  17. 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.
  18. 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
  19. 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
  20. • 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]
  21. • 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))
  22. • 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
  23. 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
  24. 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
  25. 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
  26. 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 ….
  27. 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
  28. • 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>>
  29. 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
  30. 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]
  31. 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
  32. 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 }
  33. 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
  34. 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) { … } }
  35. 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]
  36. 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
  37. 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
  38. 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)))
  39. 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
  40. Thank you! Questions? Alexey Novakov email: - alexey.novakov at ultratendency.com

    - novakov.alex at gmail.com Twitter: @alexey_novakov