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