Polymorphism
Petra Bierleutgeb
@pbvie
in Scala
Polymorphism
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
Different kinds
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 <<<
Parametric Polymorphism
aka Generics
# Parametric Polymorphism
- sometimes called Generics
- generic functions/methods
- generic data types
- implementation/behavior does not depend on type
- maintains type-safety
class Box[T](private val element: T) {
def peek(): T = element
}
# Type Safety
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
# Type Safety
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
# Type Safety
List[Dog]
def second[T](l: List[T]): Option[T] = l match {
case _ :: second :: _ => Some(second)
case _ => None
}
val secondDog: Option[Dog] = second(listOfDogs)
- type param T is bound to type Dog when we pass listOfDogs to the method
def second(l: List[Dog]): Option[Dog] = ...
polymorphic function
# Conclusion
- parametric polymorphism can be used to abstract over types
- makes programs more expressive and improves type-safety
- implementation is independent of the type parameter
Ad-hoc Polymorphism
Overloading
# Ad-hoc Polymorphism
- polymorphic methods that accept arguments of different types
- aka function/operator/method overloading
- compiler selects implementation based on type
- unlike parametric polymorphism: implementations for different types may do different things
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
# Ad-hoc Polymorphism
- overloaded methods can have a different number of parameters, in a different order, and with different types
- a method is therefore uniquely identified by a combination of all these characteristics
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)
...
# Ad-hoc Polymorphism
-
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:
# Ad-hoc Polymorphism
// 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
# Early-Binding
- overloading/dispatching is decided at compile-time
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}"
# Early-Binding
- What is the output of this program?
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
# Early-Binding
- What is the output of the program?
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
# Conclusion
- method overloading
- implementation selected based on type of parameters
- called method is decided at compile-time (early binding)
- overloading for parameterized types suffers from type erasure
- in Scala: ad-hoc polymorphism often encountered in form of the Typeclass pattern
Subtyping
Inclusion Polymorphism
# Subtyping Polymorphism
- relation between types that requires that we can
use some subtype T wherever its supertype S is expected - in Scala, such relationships are described by extending traits and/or classes
# Subtyping 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"
}
- subtypes must provide implementations for all abstract fields and methods defined on the supertype
- fields/methods already defined on the supertype can be overriden
# Subtyping Polymorphism
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)
# Late Binding
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
# Late Binding
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
# Early/Late Binding
- Scala has
- early binding for method parameters
- late binding for the object on which the method is called
# Subtyping: good parts
- good for modeling ADTs
- pattern using sealed traits/abstract classes and final case classes
- easy to add new data without modifying existing code
# Subtyping: not-so-good parts
- modify existing source code when adding new functionality
- can't add traits on external code
- only one implementation per type
- complex/inflexible hierarchies
- the (in)famous return-current-type problem
Variance and Type Bounds
Fine-tuning
# Variance
- define subtyping relationships between types with type parameters
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] {
...
}
- if there's a Box[A] and B is a subtype of A, should Box[B] be a subtype of Box[A]?
# Invariance
- there's no relation between Box[A] and Box[B], even if there's a sub/supertype relationship between A and B
- notation: [A] (default in Scala)
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!
- in general: if a type appears as produced value and consumed value it has to be invariant
# Covariance
- Box[B] is a subtype of Box[A], if B is a subtype of A
- notation: [+A]
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
}
- in general: if a type only appears as produced value (as return type), it can be made covariant
# Contravariance
- Box[B] is a supertype of Box[A], if B is a subtype of A
- notation: [-A]
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)
- in general: if a type only appears as consumed value (as parameter type), it can be made contravariant
- limits the allowed types for a type parameter
- [A <: B] A must be a subtype of B
- [A >: B] A must be a supertype of B
# Type Bounds
Type Classes
The Pattern
# What is a Typeclass
- first appeared in Haskell
- form of ad-hoc polymorphism
- Philip Wadler and Stephen Blott:
How to make ad-hoc polymorphism less ad hoc
- Philip Wadler and Stephen Blott:
- allows implementing an interface for possibly unrelated types
- existing code doesn't have to be modified when adding new data types or implementing functionality for new types
- Typeclasses in Scala: a pattern, not a language feature
# Using Typeclasses
- Scala standard lib defines some Typeclasses along with instances for common types, e.g. Ordering[T]
// 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
-
# Implementing Ordering[T] for Cat
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
# Defining Typeclasses
- The full Typeclass pattern includes
- the Typeclass itself: interface (trait) with type parameter
- Typeclass instances
- interface to use the Typeclass
- enriched interfaces
:: Example ::
Describable[T]
# Describable[T]
- Describable[T] provides a single method `describe` that returns an informal description of T
- Goal:
// 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.
# The Typeclass Interface
- interface/trait that takes a type parameter
trait Describable[T] {
def describe(t: T): String
}
# Typeclass Instances
- works, but not very useful yet
- explicitly select instance
- not better than writing describeCat, describeCoffee,... methods
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)
# Interface - First attempt
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)
# Interface - with implicits
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
# Organizing Implicits
- possible locations
- defined in current scope
- defined in companion object of T
- defined in companion object of Typeclass
- imported
- local and imported implicits take precedence over implicits defined in companion objects
- possible to define defaults and override them
# ContextBounds
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))
}
- often, implicit params are only passed through
object DescriptionPrinter {
def print[T: Describable](t: T) = println(Describer.describe(t))
}
- context bounds can replace an implicit parameter that takes a type parameter of type T
# ContextBounds
object Describer {
def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t)
}
- implicit parameter can still be summoned using implicitly
object Describer {
def describe[T: Describable](t: T): String = implicity[Describable[T]].describe(t)
}
# Interface - further improvements
trait Describable[T] {
def describe(t: T): String
}
object Describer {
def describe[T](t: T)(implicit d: Describable[T]): String = d.describe(t)
}
- there's clearly some repetition here
- gets worse when the typeclass has multiple methods that we want to support
# Interface
trait Describable[T] {
def describe(t: T): String
}
object Describer {
def apply[T](implicit d: Describable[T]): Describable[T] = d
}
- usage:
// usage
Describer[Cat].describe(someCat)
Describer[Coffee].describe(someCoffee)
def printDescription[T : Describable](t: T): Unit =
println(Describer[T].describe(t))
- we can get rid of the intermediate interface if we just want to call the methods defined on the typeclass
implicit instances need to be in scope
# Interface - Conclusion
- using implicit parameters we can use a common interface for a type T if
- the interface is implemented for T (typeclass instance)
- the implementation is in implicit scope
- by using implicit instances we can
- have multiple implementations for a single type
- choose an implementation by bringing the specific instance into scope
# Type Enrichment
- define an interface that acts like a method defined on the type
// interface syntax
Describer[Cat].describe(someCat)
Describer[Coffee].describe(someCoffee)
// with type enrichment
someCat.describe
someCoffee.describe
# Type Enrichment
- can be achieved using an implicit class
implicit class DescriberOps[T](val t: T) extends AnyVal {
def describe(implicit d: Describable[T]): String = d.describe(t)
}
# Putting it all together
// 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)
# Conclusion: Good Parts
- implementation of interfaces for possibly unrelated types
- multiple implementations per type (selection via implicits)
- implementation for external code
- add new data and new functionality without modifying existing code
# Typeclasses: Not-so-good parts
- Boilerplate: repetitive pattern needs to be manually implemented
- syntax improvements coming in Dotty
- Typeclass derivation (for instances)
- Hard to model some scenarios
- e.g. List of objects that are Describable
Upcoming Features
Typeclasses with less boilerplate
and nicer syntax
# Describable[T] Typeclass in Scala 2
// 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)
# Describable[T] in Scala 3
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
Typeclass Derivation
Automatic generation of typeclass instances
Wednesday June 12th, 15:30-16:15
The Shape(less) of Type Class Derivation in Scala 3
Miles Sabin
That's it!
Thank you
>> Question Time <<
Polymorphism in Scala
By Petra Bierleutgeb
Polymorphism in Scala
@ScalaDays2019
- 2,350