Polymorphism
Petra Bierleutgeb
@pbvie
in Scala
A single interface/method/function can be used with different types and...
many
forms
...choose a specific implementation based on the provided type
...work independently of the provided type, while still providing full type-safety
of polymorphism
Polymorphism
Ad-hoc Polymorphism
Parametric Polymorphism
Classification
Christopher Strachey
1967
Polymorphism
Ad-hoc Polymorphism
Parametric Polymorphism
Classification
polymorphic function that acts differently depending on the type of the given argument(s)
generic function or data type that works independently of the actual type - while still providing type safety
Polymorphism
Ad-hoc
Polymorphism
Parametric
Polymorphism
Classification
Peter Wegner and Luca Cardelli
1985
Inclusion Polymorphism
(subtyping)
Polymorphism
Ad-hoc
Polymorphism
Parametric
Polymorphism
Classification
Inclusion Polymorphism
(subtyping)
allows to use a subtype S of T
wherever a T is required
Essential Scala
(kindly provided by underscore)
>>> Book Tip <<<
aka Generics
class Box[T](private val element: T) { def peek(): T = element }
class Box(private val element: Any) { def peek: Any = element } val i: Int = 3 val b = new Box(i) val peeked: Any = b.peek
class Box[T](private val element: T) { def peek: T = element } val i: Int = 3 val b = new Box(i) val peeked: Int = b.peek
with parametric polymorphism
without parametric polymorphism
case class Dog(name: String, age: Int, favoriteToy: String) val listOfDogs: List[Dog] = List( Dog("max", 2, "ball"), ... ) listOfDogs.foreach(dog => println(s"${dog.name}'s favorite toy is ${dog.favoriteToy}"))
List[T] is a polymorphic data type
compiler knows element dog is of type Dog
List[Dog]
def second[T](l: List[T]): Option[T] = l match { case _ :: second :: _ => Some(second) case _ => None } val secondDog: Option[Dog] = second(listOfDogs)
def second(l: List[Dog]): Option[Dog] = ...
polymorphic function
Overloading
def combine(a: Int, b: Int): Int = a + b def combine(a: String, b: String): String = s"$a$b" def combine(a: Boolean, b: Boolean): Boolean = a && b combine(1, 3) combine("abc", "def") combine(true, false) combine(List(123), List(456)) // ooops...this one won't compile
def combine(a: Int, b: Int): Int = a + b def combine(a: String, b: String): String = s"$a$b" def combine(a: Boolean, b: Boolean): Boolean = a && b def combine(a: Int, b: String): String = s"${a.toString}$b" def combine(a: String, b: Int): String = s"$a${b.toString}" combine(1, 3) combine("abc", "def") combine(true, false) ...
watch out for type erasure!
// oops...doesn't compile! def combine(l: List[Int]): String = l.sum.toString def combine(l: List[String]): String = l.mkString(",")
List => String
List => String
[error] def combine(l: List[Int]): String at line 30 and [error] def combine(l: List[String]): String at line 32 [error] have same type after erasure: (l: List)String
compilation error:
// oops...doesn't compile! def combine(l: List[Int]): Int = l.sum def combine(l: List[String]): String = l.mkString(",")
List => Int
List => String
watch out for type erasure!
works...but not recommended
trait Animal { def name: String } final case class Dog(name: String) extends Animal final case class Cat(name: String) extends Animal def greeting(a: Animal): String = s"hello animal named ${a.name}" def greeting(d: Dog): String = s"hello dog named ${d.name}" def greeting(c: Cat): String = s"hello cat named ${c.name}"
def greeting(a: Animal): String = s"hello animal named ${a.name}" def greeting(d: Dog): String = s"hello dog named ${d.name}" def greeting(c: Cat): String = s"hello cat named ${c.name}"
val d1: Dog = Dog("max") val d2: Animal = Dog("max") val c1: Cat = Cat("garfield") println(greeting(d1)) println(greeting(d2)) println(greeting(c1))
hello dog named max hello dog named max hello cat named garfield
hello dog named max hello animal named max hello cat named garfield
A
B
def greeting(a: Animal): String = s"hello animal named ${a.name}" def greeting(d: Dog): String = s"hello dog named ${d.name}" def greeting(c: Cat): String = s"hello cat named ${c.name}"
val d1: Dog = Dog("max") val d2: Animal = Dog("max") val c1: Cat = Cat("garfield") println(greeting(d1)) println(greeting(d2)) println(greeting(c1))
hello dog named max hello animal named max hello cat named garfield
B
Inclusion Polymorphism
trait Animal { def name: String def greeting: String = s"hello animal named $name" } case class Dog(name: String) extends Animal { override val greeting: String = s"hello dog named $name" } case class Cat(name: String) extends Animal { override val greeting: String = s"hello cat named $name" }
def greet(toBeGreeted: Animal): Unit = println(toBeGreeted.greeting)
val d1: Dog = Dog("max") val d2: Animal = Dog("max") val c1: Cat = Cat("garfield") greet(d1) greet(d2) greet(c1)
def greet(toBeGreeted: Animal): Unit = println(toBeGreeted.greeting)
val d1: Dog = Dog("max") val d2: Animal = Dog("max") val c1: Cat = Cat("garfield") greet(d1) greet(d2) greet(c1)
What's the output of this program?
hello dog named max hello dog named max hello cat named garfield
hello dog named max hello animal named max hello cat named garfield
B
A
def greet(toBeGreeted: Animal): Unit = println(toBeGreeted.greeting)
val d1: Dog = Dog("max") val d2: Animal = Dog("max") val c1: Cat = Cat("garfield") greet(d1) greet(d2) greet(c1)
What's the output of this program?
hello dog named max hello dog named max hello cat named garfield
A
Fine-tuning
sealed trait Animal { def name: String } final case class Dog(name: String, favoriteToy: String) extends Animal final case class Cat(name: String, favoriteFood: String) extends Animal trait Box[T] { ... }
trait Box[A] { def put(a: A): Unit def peek(): A } // can we use a Box[Dog] wherever we expect a Box[Animal]? val dogBox: Box[Dog] = ... val myAnimal: Animal = dogBox.peek // that's ok val myCat: Cat = ... def putAnimalInsideBox(animal: Animal, box: Box[Animal]) = ... putAnimalInsideBox(myCat, dogBox) // that doesn't look good!
class Box[+A](element: A) { def peek: A = element } def runIt(): Unit = { val bA: Box[Animal] = new Box(a) val bD: Box[Dog] = new Box(d) val bX: Box[Animal] = bD // works val myAnimal: Animal = bD.peek // it's ok to store a Dog in myAnimal }
trait Greeter[-A] { def greet(a: A): String } class AnimalGreeter extends Greeter[Animal] { override def greet(a: Animal): String = s"hello ${a.name}" } def doGreeting(greeter: Greeter[Dog]): Unit = { println(greeter.greet(dog)) } val greeter = new AnimalGreeter doGreeting(greeter)
The Pattern
// simplified trait Ordering[T] { def compare(x: T, y: T): Int }
we can implement instances of a typeclass for
our own types
third-party types
and have multiple/different implementations per type
case class Cat(name: String, age: Int) implicit val catOrdering: Ordering[Cat] = new Ordering[Cat] { override def compare(x: Cat, y: Cat): Int = x.age - y.age }
val unsortedCats = List(Cat("a", 3), Cat("b", 1), Cat("c", 7)) val sortedCats = unsortedCats.sorted(catOrdering) // sorted takes an implicit ord: Ordering[B] so we could actually write val alsoSortedCats = unsortedCats.sorted
Describable[T]
// unrelated data types case class Cat(name: String, age: Int) case class Coffee(name: String, origin: String) val garfield = Cat("Garfield", 3) val coffee = Coffee("Yirgacheffe", "Ethiopia") println(garfield.describe) // Cats: Furry rulers of the world. This one is named Garfield. println(Describable[Coffee].describe(coffee)) // Coffee: Beans of Life. This one is from Ethiopia.
trait Describable[T] { def describe(t: T): String }
val catDescribable: Describable[Cat] = new Describable[Cat] { override def describe(c: Cat): String = s"Cats: Furry rulers of the world. This one is named ${c.name}" } val coffeeDescribable: Describable[Coffee] = new Describable[Coffee] { override def describe(c: Coffee): String = s"Coffee: Beans of Life. This one is from ${c.origin}" } val cat = Cat(...) val coffee = Coffee(...) catDescribable.describe(cat) coffeeDescribable.describe(coffee)
trait Describable[T] { def describe(t: T): String } val catDescribable: Describable[Cat] = ... val coffeeDescribable: Describable[Coffee] = ... object Describer { def describe[T](t: T, d: Describable[T]): String = d.describe(t) } val cat: Cat = ... val coffee: Coffee = ... Describer.describe(cat, catDescribable) Describer.describe(coffee, coffeeDescribable)
implicit val catDescribable: Describable[Cat] = ... implicit val coffeeDescribable: Describable[Coffee] = ... object Describer { def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t) } val cat: Cat = ... val coffee: Coffee = ... Describer.describe(cat) Describer.describe(coffee)
we can pass an instance of any type T for which we have an implicit Describable[T] in scope
object Describer { def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t) } object DescriptionPrinter { def print[T](t: T)(implicit d: Describable[T]) = println(Describer.describe(t)) }
object DescriptionPrinter { def print[T: Describable](t: T) = println(Describer.describe(t)) }
object Describer { def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t) }
object Describer { def describe[T: Describable](t: T): String = implicity[Describable[T]].describe(t) }
trait Describable[T] { def describe(t: T): String } object Describer { def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t) }
trait Describable[T] { def describe(t: T): String } object Describer { def apply[T](implicit d: Describable[T]): Describable[T] = d }
// usage Describer[Cat].describe(someCat) Describer[Coffee].describe(someCoffee) def printDescription[T : Describable](t: T): Unit = println(Describer[T].describe(t))
implicit instances need to be in scope
// interface syntax Describer[Cat].describe(someCat) Describer[Coffee].describe(someCoffee) // with type enrichment someCat.describe someCoffee.describe
implicit class DescriberOps[T](val t: T) extends AnyVal { def describe(implicit d: Describable[T]): String = d.describe(t) }
// typeclass trait Describable[T] { def describe(t: T): String } // interface object Describer { def apply[T](implicit d: Describable[T]): Describable[T] = d } // type enrichment implicit class DescriberOps[T](val t: T) extends AnyVal { def describe(implicit d: Describable[T]): String = d.describe(t) } // instances implicit val catDescribable: Describable[Cat] = ... implicit val coffeeDescribable: Describable[Coffee] = ... // usage val garfield = Cat("garfield", 3) println(garfield.describe)
Typeclasses with less boilerplate
and nicer syntax
// typeclass trait Describable[T] { def describe(t: T): String } // interface object Describer { def apply[T](implicit d: Describable[T]): Describable[T] = d } // type enrichment implicit class DescriberOps[T](val t: T) extends AnyVal { def describe(implicit d: Describable[T]): String = d.describe(t) } // instances implicit val catDescribable: Describable[Cat] = ... implicit val coffeeDescribable: Describable[Coffee] = ... // usage val garfield = Cat("garfield", 3) println(garfield.describe)
trait Describable[T] { def (t: T) describe: String }
object Describable { def apply[T] given Describable[T] = the[Describable[T]] }
delegate for Describable[Cat] { def (c: Cat) describe: String = ... }
val c1 = Cat("garfield", 3) println(c1.describe)
new extension method syntax
replaces implicit classes
new keywords given and the replace implicit param and implicitly
new delegate syntax replaces definition of implicit object or val for instances
usage unchanged
Automatic generation of typeclass instances
Wednesday June 12th, 15:30-16:15
The Shape(less) of Type Class Derivation in Scala 3
Miles Sabin
Thank you
>> Question Time <<