Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

From Subtype Polymorphism To Typeclass-based Ad...

From Subtype Polymorphism To Typeclass-based Ad hoc Polymorphism - An Example

From Subtype Polymorphism To Typeclass-based Ad hoc Polymorphism- An Example

Keywords: ad hoc polymorphism, context bound, context parameter, functional programming, givens, ordering, polymorphism, scala, sorting, subtype polymorphism, typeclass, using

Philip Schwarz

October 27, 2024
Tweet

More Decks by Philip Schwarz

Other Decks in Programming

Transcript

  1. From Subtype Polymorphism To Typeclass-based Ad hoc Polymorphism An Example

    @philip_schwarz slides by https://fpilluminated.com/
  2. trait Orderable[A]: def compare(other: A): Int object Orderable: extension [A](l:

    Orderable[A]) def <(r: A): Boolean = l.compare(r) < 0 def order(ns: List[Int]): List[Int] = ns match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension (ns: List[Int]) def ordered: List[Int] = order(ns) Here is a function that orders a list of integers using quicksort. @philip_schwarz assert( List(4, 1, 3, 5, 2).ordered == List(1, 2, 3, 4, 5) ) case class Person(name: String, age: Int) What if we want to order people, e.g. by their age? Let’s define a type to be Orderable if it provides a function for comparing its values so as to determine their relative order. case class Person(name: String, age: Int) extends Orderable[Person]: override def compare(other: Person): Int = this.age - other.age And now let’s make Person an Orderable assert( Person("John", 25) < Person("Jane", 30) ) Returns a negative integer, zero, or a positive integer as this is less than, equal to, or greater than other. Younger people come first Getting Person to implement (mix-in) the Orderable trait.
  3. def order[A <: Orderable[A]](as: List[A]): List[A] = as match case

    Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension [A <: Orderable[A]](as: List[A]) def ordered: List[A] = order(as) def order(ns: List[Int]): List[Int] = ns match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension (ns: List[Int]) def ordered: List[Int] = order(ns) Now let’s use subtype polymorphism and modifyi our order function so that rather than sorting a list of integers, it sorts a list of any type that is an Orderable. val people = List( Person("Jane", 30), Person("John", 25), Person("Jim", 18) ) val peopleByIncreasingAge = List( Person("Jim", 18), Person("John", 25), Person("Jane", 30) ) assert(people.ordered == peopleByIncreasingAge) Let’s sort some people. [A <: Orderable[A]] means that the order function operates on lists whose element type A is a subtype of upper bound Orderable.
  4. Now we are able to sort people, but we are

    no longer able to sort integers, because Int does not implement (mix-in) the Orderable trait, and we cannot make it do so, because we don’t own the Int type. Enter typeclasses, which allow a more general solution that does not have this limitation. Let’s define a class called Order, whose instance for type A provides a function for comparing A values so as to determine their relative order. trait Orderable[A]: def compare(other: A): Int object Orderable: extension [A](l: Orderable[A]) def <(r: A): Boolean = l.compare(r) < 0 trait Order[A]: def compare(l: A, r: A): Int object Order: extension [A](l: A)(using order: Order[A]) def <(r: A): Boolean = order.compare(l, r) < 0 (using order: Order[A]) is a context parameter, often simply called a ‘given’.
  5. Now let’s use typeclass-based ad hoc polymorphism, and modify our

    order function so that rather than ordering a list of any type that is Orderable, it sorts a list of any type for which there exists a ’given’ instance of the typeclass called Order. def order[A: Order](as: List[A]): List[A] = as match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension [A: Order](as: List[A]) def ordered: List[A] = order(as) def order[A <: Orderable[A]](as: List[A]): List[A] = as match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension [A<: Orderable[A]](as: List[A]) def ordered: List[A] = order(as) [A: Order] is a context bound, which is a shorthand syntax for (using order: Order[A]), so it means that function order has a context parameter of type Order[A].
  6. To sort some integers we need to define a ’given’

    instance of Order for Int. given Order[Int] with override def compare(l: Int, r: Int): Int = l - r We decided not to give the instance a name. if we need to get hold of the instance, we can do so using the summon function val order: Order[Int] = summon[Order[Int]] To make summoning the instance more convenient, let’s define the following apply function in the Order companion object def apply[A](using orderable: Order[A]): Order[A] = orderable val ascendingIntOrder: Order[Int] = Order[Int]
  7. val ints = List(4, 1, 3, 5, 2) val ascendingInts

    = List(1, 2, 3, 4, 5) Let’s sort some integers assert(ints.ordered == ascendingInts) assert(ints.ordered(using ascendingIntOrder) == ascendingInts) assert(ints.ordered(using Order[Int]) == ascendingInts) Our ’given’ instance of typeclass Order[Int] is implicitly passed as a parameter into the ordered and order functions. If for some reason we want to pass the parameter explicitly, here are two ways we can do it. assert(ints.ordered(using _ - _) == ascendingInts) Interestingly, an anonymous function that subtracts its two parameters also qualifies as an explicit Order[Int] parameter (see next slide).
  8. assert(ints.ordered(using _ - _) == ascendingInts) 8.9 “SAM” types …

    as in Java, Scala will allow a function type to be used where an instance of a class or trait declaring a single abstract method (SAM) is required. This will work with any SAM. For example, you might define a trait, Increaser, with a single abstract method, increase: trait Increaser: def increase(i: Int): Int You could then define a method that takes an Increaser: def increaseOne(increaser: Increaser): Int = increaser.increase(1) To invoke your new method, you could pass in an anonymous instance of trait Increaser, like this: increaseOne( new Increaser: def increase(i: Int): Int = i + 7 ) In Scala versions 2.12 and greater, however, you could alternatively just use a function literal, because Increaser is a SAM type: increaseOne(i => i + 7) // Scala trait Order[A]: def compare(l: A, r: A): Int def order[A: Order](as: List[A]): List[A] = … extension [A: Order](as: List[A]) def ordered: List[A] = order(as) Because a SAM type is expected here, we can pass in a function literal, e.g. _ - _ Order is a SAM type (declares a SAM) ordered, and order take an implicit parameter that is a SAM type
  9. Now in the Order companion object, let’s add a function

    that given an Order which orders elements in a certain way, returns a new Order that orders elements in the opposite way. extension [A](order: Order[A]) def reverse: Order[A] = (l: A, r: A) => order.compare(r, l) val descendingIntOrder = ascendingIntOrder.reverse assert(ints.ordered(using descendingIntOrder) == ascendingInts.reverse) assert(ints.ordered(using Order[Int].reverse) == ascendingInts.reverse) assert(ints.ordered(using (n1, n2) => n2 - n1) == ascendingInts.reverse) We can now order integers using the reverse of ’given’ Order[Int], i.e. in descending order. Note that we are comparing r with l, rather than the other way around.
  10. Let’s do for String what we have done so far

    for Int. To order Strings we define a ’given’ instance of Order for String. given Order[String] with override def compare(l: String, r: String): Int = l.compareTo(r) val names = List("John", "Jane", "Jim") val ascendingNames = List("Jane", "Jim", "John") val ascendingStringOrder = Order[String] assert(names.ordered == ascendingNames) assert(names.ordered(using ascendingStringOrder) == ascendingNames) assert(names.ordered(using Order[String]) == ascendingNames) assert(names.ordered(using _ compareTo _) == ascendingNames) val descendingStringOrder = ascendingStringOrder.reverse assert(names.ordered(using descendingStringOrder) == ascendingNames.reverse) assert(names.ordered(using Order[String].reverse) == ascendingNames.reverse) assert(names.ordered(using (s1,s2) => s2 compareTo s1) == ascendingNames.reverse)
  11. case class Person(name: String, age: Int) case class Person(name: String,

    age: Int) extends Orderable[Person]: override def compare(other: Person): Int = this.age - other.age Now let’s move on to ordering people. We begin by undoing the changes that we made to Person in order to use subtype polymorphism., i.e. we stop Person from implementing (mixing-in) Orderable. With Int, it was natural for the ‘given’ Order[Int] to be ascending order. Similarly with String. But what should be the default ordering for people? By ascending age?, By ascending name? There isn’t a natural order for people, so we are not going to provide a default. Also, since age is an Int and name is a String, and since ‘givens’ Order[Int] and Order[String] already exist, instead of defining an Order[Person] from scratch, we are going to define it in terms of one of those (see next slide).
  12. extension [B](order: Order[B]) def on[A](f: A => B): Order[A] =

    (l: A, r: A) => order.compare(f(l), f(r)) If we have an Order[B] for ordering Bs, and we have a function that converts an A to a B, then we can easily create an Order[A] for ordering As.
  13. val people = List(Person("Jane", 30), Person("John", 25), Person("Jim", 18)) val

    peopleByAge = List(Person("Jim", 18), Person("John", 25), Person("Jane", 30)) given ageOrder: Order[Person] = ascendingIntOrder.on(_.age) assert(Person("John", 25) < Person("Jane", 30)) Let’s define some people. Let’s say that locally to a certain part of the application, we do want ordering by age to be the default. Create a Person order that is an ascending Int order on a person’s age. assert(people.ordered == peopleByAge) assert(people.ordered(using ageOrder) == peopleByAge) assert(people.ordered(using Order[Int].on[Person](_.age)) == peopleByAge) assert(people.ordered(using (p1, p2) => p1.age - p2.age) == peopleByAge) Now let’s order the people by ascending age. assert(people.ordered(using ageOrder.reverse) == peopleByAge.reverse) assert(people.ordered(using Order[Int].on[Person](_.age).reverse) == peopleByAge.reverse) assert(people.ordered(using (p1, p2) => p2.age - p1.age) == peopleByAge.reverse) And now by descending age Younger people come first
  14. val people = List(Person("Jane", 30), Person("John", 25), Person("Jim", 18)) val

    peopleByName = List(Person("Jane", 30), Person("Jim", 18), Person("John", 25)) val lexicographicStringOrder = Order[String] val nameOrder: Order[Person] = lexicographicStringOrder.on(_.name) Same as on the previous slide, but here we are ordering people by name. Create a Person order that is an ascending String order on a person’s name. assert(people.ordered == peopleByName) assert(people.ordered(using nameOrder) == peopleByName) assert(people.ordered(using Order[String].on[Person](_.name)) == peopleByName) assert(people.ordered(using (p1,p2) => p1.name - p2.name) == peopleByName) First let’s order the people by ascending name, and then by descending name. assert(people.ordered(using nameOrder.reverse) == peopleByName.reverse) assert(people.ordered(using Order[String].on[Person](_.age).reverse) == peopleByName.reverse) assert(people.ordered(using (p1, p2) => p2.name - p1.name) == peopleByName.reverse)
  15. To conclude the example, let’s say that we want to

    order people first by age, and then by name, i.e. we want to order them by age, but when the age is the same, we want to order them by name. Rather than defining a ‘given’ ordering for this specific case, we are going to do the following: 1. define a generic ‘given’ Order[(A, B)] for any A and B for which ‘givens’ Order[A] and Order[B] exist. 2. define our desired Order[Person] applying our existing on function to Order[(Int,String)] (the latter exists because ‘givens’ Order[Int] and Order[String] exist). given [A, B](using oa: Order[A], ob: Order[B]): Order[(A, B)] with override def compare(l: (A, B), r: (A, B)): Int = (l, r) match case ((la, lb), (ra, rb)) => val asComparison = oa.compare(la, ra) if asComparison == 0 then ob.compare(lb, rb) else asComparison
  16. val morePeople = List(Person("John", 25), Person("Jane", 30), Person("Jack", 25), Person("Jim",

    18)) val morePeopleByAgeAndThenName = List(Person("Jim", 18), Person("Jack", 25), Person("John", 25), Person("Jane", 30)) val ageAndThenNameOrder: Order[Person] = Order[(Int,String)].on(p => (p.age, p.name)) assert(morePeople.ordered(using ageAndThenNameOrder) == morePeopleByAgeAndThenName) Now we can order people as desired. Note that John and Jack are the same age.
  17. def order(ns: List[Int]): List[Int] = ns match case Nil =>

    Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension (ns: List[Int]) def ordered: List[Int] = order(ns) Ordering integers assert( List(4, 1, 3, 5, 2).ordered == List(1, 2, 3, 4, 5) )
  18. trait Orderable[A]: def compare(other: A): Int object Orderable: extension [A](l:

    Orderable[A]) def <(r: A): Boolean = l.compare(r) < 0 case class Person(name: String, age: Int) extends Orderable[Person]: override def compare(other: Person): Int = this.age - other.age def order[A <: Orderable[A]](as: List[A]): List[A] = as match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension [A <: Orderable[A]](as: List[A]) def ordered: List[A] = order(as) Ordering people using subtype polymorphism val people = List( Person("Jane", 30), Person("John", 25), Person("Jim", 18) ) val peopleByIncreasingAge = List( Person("Jim", 18), Person("John", 25), Person("Jane", 30) ) assert(people.ordered == peopleByIncreasingAge)
  19. trait Order[A]: def compare(l: A, r: A): Int object Order:

    def apply[A](using orderable: Order[A]): Order[A] = orderable extension [A](l: A)(using order: Order[A]) def <(r: A): Boolean = order.compare(l, r) < 0 extension [A](order: Order[A]) def reverse: Order[A] = (l: A, r: A) => order.compare(r, l) extension [B](order: Order[B]) def on[A](f: A => B): Order[A] = (l: A, r: A) => order.compare(f(l), f(r)) def order[A: Order](as: List[A]): List[A] = as match case Nil => Nil case head :: tail => val (smaller, larger) = tail.partition(_ < head) order(smaller) ++ List(head) ++ order(larger) extension [A: Order](as: List[A]) def ordered: List[A] = order(as) given Order[Int] with override def compare(l: Int, r: Int): Int = l – r given Order[String] with override def compare(l: String, r: String): Int = l.compareTo(r) given [A,B](using oa:Order[A], ob:Order[B]):Order[(A,B)] with override def compare(l: (A, B), r: (A, B)): Int = (l, r) match case ((la, lb), (ra, rb)) => val asComparison = oa.compare(la, ra) if asComparison == 0 then ob.compare(lb, rb) else asComparison case class Person(name: String, age: Int) Ordering integers, strings and people using typeclass-based ad hoc polymorphism
  20. val order: Order[Int] = summon[Order[Int]] val ascendingIntOrder: Order[Int] = Order[Int]

    val ints = List(4, 1, 3, 5, 2) val ascendingInts = List(1, 2, 3, 4, 5) assert(ints.ordered == ascendingInts) assert(ints.ordered(using ascendingIntOrder) == ascendingInts) assert(ints.ordered(using Order[Int]) == ascendingInts) assert(ints.ordered(using _ - _) == ascendingInts) val descendingIntOrder = ascendingIntOrder.reverse assert(ints.ordered(using descendingIntOrder) == ascendingInts.reverse) assert(ints.ordered(using Order[Int].reverse) == ascendingInts.reverse) assert(ints.ordered(using (n1, n2) => n2 - n1) == ascendingInts.reverse)
  21. val names = List("John", "Jane", "Jim") val ascendingNames = List("Jane",

    "Jim", "John") val ascendingStringOrder = Order[String] assert(names.ordered == ascendingNames) assert(names.ordered(using ascendingStringOrder) == ascendingNames) assert(names.ordered(using Order[String]) == ascendingNames) assert(names.ordered(using _ compareTo _) == ascendingNames) val descendingStringOrder = ascendingStringOrder.reverse assert(names.ordered(using descendingStringOrder) == ascendingNames.reverse) assert(names.ordered(using Order[String].reverse) == ascendingNames.reverse) assert(names.ordered(using (s1,s2) => s2 compareTo s1) == ascendingNames.reverse)
  22. val people = List(Person("Jane", 30), Person("John", 25), Person("Jim", 18)) val

    peopleByAge = List(Person("Jim", 18), Person("John", 25), Person("Jane", 30)) given ageOrder: Order[Person] = ascendingIntOrder.on(_.age) assert(Person("John", 25) < Person("Jane", 30)) assert(people.ordered == peopleByAge) assert(people.ordered(using ageOrder) == peopleByAge) assert(people.ordered(using Order[Int].on[Person](_.age)) == peopleByAge) assert(people.ordered(using (p1, p2) => p1.age - p2.age) == peopleByAge) assert(people.ordered(using ageOrder.reverse) == peopleByAge.reverse) assert(people.ordered(using Order[Int].on[Person](_.age).reverse) == peopleByAge.reverse) assert(people.ordered(using (p1, p2) => p2.age - p1.age) == peopleByAge.reverse)
  23. val people = List(Person("Jane", 30), Person("John", 25), Person("Jim", 18)) val

    peopleByName = List(Person("Jane", 30), Person("Jim", 18), Person("John", 25)) val lexicographicStringOrder = Order[String] val nameOrder: Order[Person] = lexicographicStringOrder.on(_.name) assert(people.ordered == peopleByName) assert(people.ordered(using nameOrder) == peopleByName) assert(people.ordered(using Order[String].on[Person](_.name)) == peopleByName) assert(people.ordered(using (p1,p2) => p1.name - p2.name) == peopleByName) assert(people.ordered(using nameOrder.reverse) == peopleByName.reverse) assert(people.ordered(using Order[String].on[Person](_.age).reverse) == peopleByName.reverse) assert(people.ordered(using (p1, p2) => p2.name - p1.name) == peopleByName.reverse)
  24. val morePeople = List(Person("John", 25), Person("Jane", 30), Person("Jack", 25), Person("Jim",

    18)) val morePeopleByAgeAndThenName = List(Person("Jim", 18), Person("Jack", 25), Person("John", 25), Person("Jane", 30)) val ageAndThenNameOrder: Order[Person] = Order[(Int,String)].on(p => (p.age, p.name)) assert(morePeople.ordered(using ageAndThenNameOrder) == morePeopleByAgeAndThenName)