Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

- 4yrs in Scala, 10yrs in Java - a fan of Data Processing, - Distributed Systems, - Functional Programming ALEXEY NOVAKOV Senior Consultant bei INNOQ Deutschland GmbH

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Solution Type Classes

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Function is defined for a range of types, but acting in different way for each type T * T “ad-hoc polymorphism”

Slide 10

Slide 10 text

Serializable Eq Hashable Printable A B C D Traits/Interfaces Classes Subtyping (Hierarchical)

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Type Class Implementation

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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)

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

“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

Slide 18

Slide 18 text

Scala type class ingredients

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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.

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

When to use Type Classes

Slide 32

Slide 32 text

• 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]

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

• 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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

• 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 >

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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]

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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 }

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Current Drawbacks

Slide 49

Slide 49 text

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) { … } }

Slide 50

Slide 50 text

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]

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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