(16) Polymorphism

(16) Polymorphism

Polymorphism

Intro

Polymorphism provides a single interface to entities of different types or a single symbol to represent multiple different types.

In programming, it means that

  • the function can be applied to arguments of many types, or
  • the type can have instances of many types.

Polymorphism in Biology

Polymorphism is borrowed from a principle in biology where an organism or species can have many different forms or stages.

image source: https://www.biologyonline.com/dictionary/polymorphism

file

Categories

1/3 Adhoc polymorphism – Overloading
2/3 Parametric polymorphism – Generic – Instances of a function or class are created by type parameterization
3/3 Subtyping – Instances of a subclass can be passed to a base class

Polymorphism 1/3 Adhoc – Overloading

Overloading lets you declare the same method multiple times with different argument lists.

image source: https://www.guru99.com/cpp-polymorphism.html.
file


// Creating a super class  
abstract class Shapes {  
  def draw() = println("Draw the shape")  

  // Overload  
  def draw(id: Int) = println(s"Draw the shape (Id: $id)")  
}  

// Creating a subclass  
class Rectangle extends Shapes  

// Creating a subclass  
class Circle extends Shapes {  
  // Overriding   
 override def draw() = println("Draw the Circle")  

  // Override, Overload  
  override def draw(id: Int) = println(s"Draw the Circle (Id: $id)")  
}  

val rectangle = new Rectangle  
rectangle.draw()  
rectangle.draw(0)  

val circle = new Circle  
circle.draw()  
circle.draw(1)

Result:

val rectangle: Rectangle = Rectangle@3aa66b06
Draw the shape
Draw the shape (Id: 0)

val circle: Circle = Circle@6d6d7a8
Draw the Circle
Draw the Circle (Id: 1)

Overriding vs Overloading

Overloading Overriding
Must have at least two methods by the same name in the same class. Must have at least one method by the same name in both Parent and child class.
Must have a different number of parameters. If the numbers of parameters are the same, they must have different parameters. Must have the same number of Parameters.
Overloading is known as compile-time polymorphism. Types of parameters also must be the same.

Polymorphism 2/3 Parametric – Generic

Algorithms are written in terms of types to be specified later that are then instantiated when needed for specific types provided as parameters.

i.e. Generic classes/functions would take a type as a parameter.

class Stack[A] {  
  private var elements: List[A] = Nil  
  def push(x: A): Unit =  
    elements = x :: elements  
  def peek: A = elements.head  
  def pop(): A = {  
  val currentTop = peek  
    elements = elements.tail  
    currentTop  
  }  
}  

val stack = new Stack[Int]  
stack.push(1)  
stack.push(2)  
println(stack.pop()) // prints 2  
println(stack.pop()) // prints 1

Polymorphism 3/3 Subtyping

Intro

A form of type polymorphism in which a subtype is a datatype that is related to another datatype (the supertype) by some notion of substitutability

i.e. we can operate on the supertype elements and operate on the subtype’s elements.

Suppose S is a subtype of T. In that case, the subtyping relation is often written S <: T to mean that any term of type S can be safely used in a context where a term of type T is expected.

  • upper bound – S <: T means S is a subtype of T, and
  • lower bound – S >: T means S is a supertype of T, or T is a subtype of S.
  • Mixed bound – S >: TS <: T means restrict S any type on the interval between TS and T

image source: https://livebook.manning.com/book/programming-with-types/chapter-7/
file

Example

// Creating a super class  
class Shape  
// Creating a subclass  
class Rectangle extends Shape  

def draw[T <: Shape](shape: T): Unit = println(s"Draw the shape ${shape}")  

draw[Shape](new Shape) // Draw the shape class repl$.rs$line$1$Shape  

draw[Rectangle](new Rectangle) // Draw the shape class repl$.rs$line$2$Rectangle

Variance

Relationships

Relationship between generics, subtyping, variance and polymorphism

file

Intro

Variance refers to how subtyping between more complex types relates to subtyping between their components, i.e. Variance tells us if a type constructor (equivalent to a generic type in Java) is a subtype of another type constructor.

class  Foo[+A]  // A covariant class  
class  Bar[-A]  // A contravariant class  
class  Baz[A]  // An invariant class

Variance Checks

class Foo[+A] – covariant type parameters can only appear in method results.
class Bar[-A] – contravariant type parameters can only appear in method parameters.
class Baz[A] – invariant type parameters can appear anywhere.

Example:

trait Function1[-T, +U]:  
  def apply(x: T): U // good  

  def apply2(x: U): T // bad   
// Error msg - contravariant type T occurs in covariant position in type (x: U): T

Here,

  • T is contravariant and appears only as a method parameter type
  • U is covariant and appears only as a method result type

Covariance

A type parameter T of a generic class can be made covariant by using the annotation +T. For some class List[+T], making T covariant implies that for two types A and B where B is a subtype of A, then List[B] is a subtype of List[A]. This allows us to make very useful and intuitive subtyping relationships using generics.

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

Both Cat and Dog are subtypes of Animal. The Scala standard library has a generic immutable sealed abstract class List[+A] class, where the type parameter A is covariant. This means that a List[Cat] is a List[Animal]. A List[Dog] is also a List[Animal]. Intuitively, it makes sense that a list of cats and a list of dogs are each list of animals, and you should be able to use either of them in place of List[Animal].

In the following example, the method printAnimalNames will accept a list of animals as an argument and print their names on a new line. If List[A] were not covariant, the last two method calls would not compile, which would severely limit the usefulness of the printAnimalNames method.


def printAnimalNames(animals: List[Animal]): Unit =  
  animals.foreach {  
  animal => println(animal.name)  
 }  
val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))  
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))  

// prints: Whiskers, Tom  
printAnimalNames(cats)  

// prints: Fido, Rex  
printAnimalNames(dogs)

Contravariance

A type parameter A of a generic class can be made contravariant by using the annotation -A. This creates a subtyping relationship between the class and its type parameter that is similar but opposite to what we get with covariance. That is, for some class Printer[-A], making A contravariant implies that for two types A and B where A is a subtype of B, Printer[B] is a subtype of Printer[A].

Consider the Cat, Dog, and Animal classes defined above for the following example:

abstract class Printer[-A] {
  def print(value: A): Unit
}

A Printer[A] is a simple class that knows how to print out some type A. Let’s define some subclasses for specific types:

class AnimalPrinter extends Printer[Animal] {
  def print(animal: Animal): Unit =
    println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
  def print(cat: Cat): Unit =
    println("The cat's name is: " + cat.name)
}

If a Printer[Cat] knows how to print any Cat to the console, and a Printer[Animal] knows how to print any Animal to the console, it makes sense that a Printer[Animal] would also know how to print any Cat. The inverse relationship does not apply because a Printer[Cat] does not know how to print any Animal to the console. Therefore, we should be able to use a Printer[Animal] in place of Printer[Cat], if we wish, and making Printer[A] contravariant allows us to do exactly that.


abstract class Printer[-A] {  
  def print(value: A): Unit  
}  

class AnimalPrinter extends Printer[Animal] {  
  def print(animal: Animal): Unit =  
    println("The animal's name is: " + animal.name)  
}  

class CatPrinter extends Printer[Cat] {  
  def print(cat: Cat): Unit =  
    println("The cat's name is: " + cat.name)  
}  

def printMyCat(printer: Printer[Cat], cat: Cat): Unit =  
  printer.print(cat)  

val catPrinter: Printer[Cat] = new CatPrinter  
val animalPrinter: Printer[Animal] = new AnimalPrinter  

printMyCat(catPrinter, Cat("Boots")) // The cat's name is: Boots  
printMyCat(animalPrinter, Cat("Boots")) // The animal's name is: Boots

The output of this program will be:

The cat's name is: Boots.
The animal's name is: Boots.

Invariance

Generic classes in Scala are invariant by default. This means that they are neither covariant nor contravariant. In the following example, the Container class is invariant. A Container[Cat] is not a Container[Animal], nor is the reverse true.

class Container[A](value: A) {
  private var _value: A = value
  def getValue: A = _value
  def setValue(value: A): Unit = {
    _value = value
  }
}

It may seem like a Container[Cat] should naturally also be a Container[Animal], but allowing a mutable generic class to be covariant would not be safe. In this example, it is very important that Container is invariant. Supposing Container was actually covariant, something like this could happen:

abstract class Animal {  
  def name: String  
}  

case class Cat(name: String) extends Animal  
case class Dog(name: String) extends Animal  

class Container[A](value: A) {  
  private var _value: A = value  
  def getValue: A = _value  
  def setValue(value: A): Unit = {  
 _value = value  
  }  
}  

val catContainer: Container[Cat] = new Container(Cat("Felix"))  
val cat: Cat = catContainer.getValue // val cat: Cat = Cat(Felix)  

// Error  
val animalContainer: Container[Animal] = catContainer  
// Found: (catContainer : Container[Cat]) Required: Container[Animal]  

// Won't work  
animalContainer.setValue(Dog("Spot")) // Not found: animalContainer

Fortunately, the compiler won’t work and stops us long before we can get this far.

Comparison With Other Languages

Variance is supported in different ways by some languages similar to Scala. For example, variance annotations in Scala closely resemble those in C#, where the annotations are added when a class abstraction is defined (declaration-site variance).

In Java, however, variance annotations are given by clients when a class abstraction is used (use-site variance).

Leave a Reply

Your email address will not be published. Required fields are marked *