Type Class: The Ultimate Ad Hoc George Wilson Data61/CSIRO - - PowerPoint PPT Presentation

type class the ultimate ad hoc
SMART_READER_LITE
LIVE PREVIEW

Type Class: The Ultimate Ad Hoc George Wilson Data61/CSIRO - - PowerPoint PPT Presentation

Type Class: The Ultimate Ad Hoc George Wilson Data61/CSIRO george.wilson@data61.csiro.au August 28, 2017 Type classes are a language feature Haskell Purescript Eta Clean or sometimes a design pattern Scala Polymorphism


slide-1
SLIDE 1

Type Class: The Ultimate Ad Hoc

George Wilson

Data61/CSIRO george.wilson@data61.csiro.au

August 28, 2017

slide-2
SLIDE 2

Type classes are a language feature

◮ Haskell ◮ Purescript ◮ Eta ◮ Clean

  • r sometimes a design pattern

◮ Scala

slide-3
SLIDE 3

Polymorphism

slide-4
SLIDE 4

Something which is polymorphic has many shapes

slide-5
SLIDE 5

Polymorphism is good

◮ less duplication ◮ more reuse ◮ many other benefits

slide-6
SLIDE 6

Broadly speaking there are two major forms of polymorphism in programming:

◮ parametric polymorphism ◮ ad-hoc polymorphism

slide-7
SLIDE 7

A parametrically polymorphic type has at least one type parameter which can be instantiated to any type. Example: reverse :: [a] -> [a]

slide-8
SLIDE 8

An ad-hocly polymorphic type can be instantiated to some different types, and may behave differently for each type Example: ==

slide-9
SLIDE 9
slide-10
SLIDE 10

“[. . . ] exhibits ad-hoc polymorphism”

slide-11
SLIDE 11
slide-12
SLIDE 12
slide-13
SLIDE 13

Interfaces

slide-14
SLIDE 14

interface Equal<A> { public boolean eq(A other); }

slide-15
SLIDE 15

interface Equal<A> { public boolean eq(A other); } class Person { public int age; public String name; }

slide-16
SLIDE 16

interface Equal<A> { public boolean eq(A other); } class Person implements Equal<Person> { public int age; public String name; public boolean eq(Person other) { return this.age == other.age && this.name.equals(other.name); } }

slide-17
SLIDE 17

static <A extends Equal<A>> boolean elementOf(A a, List<A> list) { for (A element : list) { if (a.eq(element)) return true; } return false; }

slide-18
SLIDE 18

"hello".eq("he11o")

slide-19
SLIDE 19

package java.lang; class String { private char[] value; // other definitions }

slide-20
SLIDE 20

package java.lang; class String implements Equal<String> { private char[] value; // other definitions }

slide-21
SLIDE 21
slide-22
SLIDE 22

class List<A> { // implementation details }

slide-23
SLIDE 23

class List<A> implements Equal<List<A>> { // implementation details public boolean eq(List<A> other) { // implementation... } }

slide-24
SLIDE 24
slide-25
SLIDE 25
slide-26
SLIDE 26
slide-27
SLIDE 27
slide-28
SLIDE 28

class List<A> implements Equal<List<A>> { // implementation details public boolean eq(List<A> other) { // implementation... // ... but how do we compare A for equality? } }

slide-29
SLIDE 29

◮ Interface implementation can’t be conditional ◮ We can only implement interfaces for types we control

slide-30
SLIDE 30

Type Classes

slide-31
SLIDE 31

class Equal a where eq :: a -> a -> Bool

slide-32
SLIDE 32

class Equal a where eq :: a -> a -> Bool data Person = Person { age :: Int , name :: String }

slide-33
SLIDE 33

class Equal a where eq :: a -> a -> Bool data Person = Person { age :: Int , name :: String } instance Equal Person where eq p1 p2 = eq (age p1) (age p2) && eq (name p1) (name p2)

slide-34
SLIDE 34

elementOf :: Equal a => a -> [a] -> Bool elementOf a list = case list of []

  • > False

(h:t) -> eq a h || elementOf a t

slide-35
SLIDE 35

Instances can be constrained instance (Equal a) => Equal [a] where eq [] [] = True eq (x:xs) [] = False eq [] (y:ys) = False eq (x:xs) (y:ys) = eq x y && eq xs ys

slide-36
SLIDE 36

Instances can be constrained instance (Equal a) => Equal [a] where eq [] [] = True eq (x:xs) [] = False eq [] (y:ys) = False eq (x:xs) (y:ys) = eq x y && eq xs ys We can add type class instances for types we didn’t write

slide-37
SLIDE 37

Some benefits:

◮ You can write instances for types you did not write ◮ Instances can depend on other instances

Compared to Interfaces:

◮ More expressive ◮ More modular

slide-38
SLIDE 38

Type classes have restrictions in order to enforce type class coherence Informally, coherence means:

◮ for a given type class for a given type, there is zero or one instance ◮ no matter how you ask for an instance, you get the same one ◮ if an instance exists, you can’t not get it

slide-39
SLIDE 39

There are exactly two places a type class instance is allowed to exist

Person.hs

data Person = Person { age: Int , name: String } instance Equal Person where eq p1 p2 = ...

Equal.hs

class Equal a where eq :: a -> a -> Bool

slide-40
SLIDE 40

There are exactly two places a type class instance is allowed to exist

Person.hs

data Person = Person { age: Int , name: String }

Equal.hs

class Equal a where eq :: a -> a -> Bool instance Equal Person where eq p1 p2 = ...

slide-41
SLIDE 41

Person.hs

data Person = Person { age: Int , name: String }

Equal.hs

class Equal a where eq :: a -> a -> Bool

EqualInstances.hs

instance Equal Person where eq p1 p2 = ...

slide-42
SLIDE 42

Person.hs

data Person = Person { age: Int , name: String }

Equal.hs

class Equal a where eq :: a -> a -> Bool

EqualInstances.hs

instance Equal Person where eq p1 p2 = ... “Orphan instance” Orphan instances can break coherence

slide-43
SLIDE 43

Type class coherence benefits sanity:

◮ When you use a type class, the thing you expect happens ◮ Instances never depends on imports or ordering ◮ “plumbing” is done behind the scenes and can’t go wrong

slide-44
SLIDE 44

Type class coherence benefits sanity:

◮ When you use a type class, the thing you expect happens ◮ Instances never depends on imports or ordering ◮ “plumbing” is done behind the scenes and can’t go wrong

Type class coherence rules out:

◮ Custom local instances ◮ Multiple, selectable instances

(But there are other solutions to those things)

slide-45
SLIDE 45

Implicits

More Flexible Than TypeclassesTM

slide-46
SLIDE 46

case class Person(age: Int, name: String)

slide-47
SLIDE 47

case class Person(age: Int, name: String) trait Equal[A] { def eq(a: A, b: A): Boolean }

slide-48
SLIDE 48

case class Person(age: Int, name: String) trait Equal[A] { def eq(a: A, b: A): Boolean } implicit def equalPerson: Equal[Person] = new Equal[Person] { def eq(a: Person, b: Person): Boolean = a.age == b.age && a.name == b.name }

slide-49
SLIDE 49

def elementOf[A](a: A, list: List[A]) (implicit equalA: Equal[A]): Boolean = { list match { case Nil => false case (h::t) => equal.eq(a, h) || elementOf(a, t) } }

slide-50
SLIDE 50

implicit def equalList(implicit equalA: Equal[A]): Equal[List[A]] = new Equal[List[A]] { def eq(a: List[A], b: List[A]): Boolean = { (a,b) match { case (Nil, Nil) => true case (x::xs, Nil) => false case (Nil, y::ys) => false case (x::xs, y::ys) => equalA.eq(x,y) || eq(xs,ys) } } }

slide-51
SLIDE 51

◮ We can define implicits for types we did not write ◮ We can write implicits that depend on implicits

slide-52
SLIDE 52

◮ We can define implicits for types we did not write ◮ We can write implicits that depend on implicits ◮ No restriction on orphan instances ◮ No restriction on number of instances

slide-53
SLIDE 53

sealed trait Ordering case object LT extends Ordering case object EQ extends Ordering case object GT extends Ordering

slide-54
SLIDE 54

sealed trait Ordering case object LT extends Ordering case object EQ extends Ordering case object GT extends Ordering trait Order[A] { def compare(a: A, b: A): Ordering }

slide-55
SLIDE 55

sealed trait Ordering case object LT extends Ordering case object EQ extends Ordering case object GT extends Ordering trait Order[A] { def compare(a: A, b: A): Ordering } implicit def joyDivisionWithoutIan = new Order[Person] { def compare(a: Person, b: Person): Ordering = intOrder.compare(a.age, b.age) match { case LT => LT case EQ => stringOrder.compare(a.name, b.name) case GT => GT } }

slide-56
SLIDE 56

def sort[A](list: List[A])(implicit orderA: Order[A]): List[A] = { // quicksort goes here }

slide-57
SLIDE 57

sort( List( Person(30, "Robert") , Person(20, "John") , Person(30, "Alfred") ) )

slide-58
SLIDE 58

sort( List( Person(30, "Robert") , Person(20, "John") , Person(30, "Alfred") ) ) ==> List( Person(20, "John") , Person(30, "Alfred") , Person(30, "Robert") )

slide-59
SLIDE 59

Then the boss says “I want those sorted by name”.

slide-60
SLIDE 60

Then the boss says “I want those sorted by name”. implicit def orderPersonByName: Order[Person] = new Order[Person] { def compare(a: Person, b: Person): Ordering = stringOrder.compare(a.name, b.name) match { case LT => LT case EQ => intOrder.compare(a.age, b.age) case GT => GT } }

slide-61
SLIDE 61

sort( List( Person(30, "Robert") , Person(20, "John") , Person(30, "Alfred") ) )

slide-62
SLIDE 62

sort( List( Person(30, "Robert") , Person(20, "John") , Person(30, "Alfred") ) ) ==> List( Person(30, "Alfred") , Person(20, "John") , Person(30, "Robert") )

slide-63
SLIDE 63

// both in scope implicit def orderPersonByAge: Order[Person] = ... implicit def orderPersonByName: Order[Person] = ... // what happens? sort(persons)

slide-64
SLIDE 64

// both in scope implicit def orderPersonByAge: Order[Person] = ... implicit def orderPersonByName: Order[Person] = ... // what happens? sort(persons) Hopefully a compiler error!

slide-65
SLIDE 65

{1, 2, 3} ∪ {4, 5, 6}

slide-66
SLIDE 66

Set.scala

def emptySet[A]: Set[A] def insert[A](a: A, set: Set[A])(implicit o: Order[A]): Set[A] def isElement[A](a: A, set: Set[A])(implicit o: Order[A]): Boolean

slide-67
SLIDE 67

Persons.scala

implicit def orderPersonByAge: Order[Person] = ... def persons: Set[Person] = insert(p1, insert(p2, insert(p3, emptySet)))

slide-68
SLIDE 68

Persons.scala

implicit def orderPersonByAge: Order[Person] = ... def persons: Set[Person] = insert(p1, insert(p2, insert(p3, emptySet)))

Something.scala

import Persons.{p1, persons} implicit def orderPersonByName: Order[Person] = ... val x = isElement(p1, persons)

slide-69
SLIDE 69

Persons.scala

implicit def orderPersonByAge: Order[Person] = ... def persons: Set[Person] = insert(p1, insert(p2, insert(p3, emptySet)))

Something.scala

import Persons.{p1, persons} implicit def orderPersonByName: Order[Person] = ... val x = isElement(p1, persons) // FALSE!

slide-70
SLIDE 70

Recommendations when writing implicits:

◮ Only create instances in the file that defines the type or the “type class” ◮ Disallow creating more than one instance (regardless of which file you’re in)

slide-71
SLIDE 71

Recommendations when writing implicits:

◮ Only create instances in the file that defines the type or the “type class” ◮ Disallow creating more than one instance (regardless of which file you’re in)

What about implicits in external libraries?

◮ Assess their usage of implicits. Do they use them as like type classes? ◮ If you distrust their implicits, pass everything of theirs explicitly

slide-72
SLIDE 72

:)

slide-73
SLIDE 73

D.hs:11:10: error: Duplicate instance declarations: instance Ord Person -- Defined at D.hs:11:10 instance Ord Person -- Defined at D.hs:14:10

slide-74
SLIDE 74

O3.hs:6:1: warning: [-Worphans] Orphan instance: instance Equal Person To avoid this move the instance declaration to the module of the class or of the type, or wrap the type with a newtype and declare the instance on the new type. <no location info>: error: Failing due to -Werror.

slide-75
SLIDE 75

Type classes:

◮ Big wins in flexibility, expressiveness, and modularity ◮ Restrictions are straightforward and compiler checked ◮ Coherence keeps things sane

slide-76
SLIDE 76

Thanks for listening!

Aspect Interfaces Type classes Implicits Instance types you control

  • Instance types you don’t control

X

  • Instances can depend on other instances

X

  • Type-directed
  • sort of

Custom local instances X X

  • Coherent
  • X