CSC 347 - Concepts of Programming Languages

Subtyping

Instructor: Stefan Mitsch

Learning Objectives

What should the type relationship between parameterized types be?

  • Identify read and write restrictions on parametric types

Checked Casts

  • Recall lecture on safety
  1. class A { int x; }
  2. class B extends A { float y; }
  3. class C extends A { char c; }
  4. void f (B b) {
  5. A a = b; // upcast always safe
  6. }
  7. void g (A a) {
  8. B b = (B) a; // downcast must be checked
  9. }
  10. f (new B()); // OK
  11. g (new C()); // ClassCastException

What makes upcasting safe? What makes downcasting unsafe?

Subtyping

  • Static and dynamic type
    1. A x = new A ();
    2. B y = new B ();
    3. x = y; // B ok when A expected
  • Method parameters
    1. void aConsumer (A x) { ... }
    2. aConsumer (new B()); // B ok when A expected
  • Method results
    1. B bProducer () { ... }
    2. A x = bProducer (); // B ok when A expected
  • Safe to use an instance of B when an A is expected
  • B is a subtype of A: written B<:A
    • If y:B and B<:A then y:A (upcast)
  • Subtyping is not just subclassing: parametric polymorphism

Subtyping Order: Top

  • Subtyping relation <: is a partial order on types
    • reflexive: X<:X
    • transitive: if Duck<:Bird and Bird<:Animal then Duck<:Animal
  • Some PLs have a Top type: X<:Top for all X (greater than all other types)
  • In Java: java.lang.Object above reference types
  • In Scala: scala.Any above all types, scala.AnyRef above reference types
    1. val xs:List[AnyRef] = List ("hello", List(1))
    2. val ys:List[Any] = List ("hello", 1)

Subtyping Order: Bottom

  • Most PLs do not have a Bottom<:X for all X (less than all other types)
  • In Scala Type Hierarchy: Nothing is Bottom type

  • What is Bottom useful for?

  • Important for typing uses of Nil

    1. val nil:List[String] = Nil
    2. // Best type possible
    3. val xs1:List[String] = "hello" :: nil
    4. val xs2:List[Any] = 1 :: nil
    1. val nil:List[Nothing] = Nil
    2. // Best type possible
    3. val xs1:List[String] = "hello" :: nil
    4. val xs2:List[Int] = 1 :: nil
  • Nil : List[Nothing] and List[Nothing] <: List[X], so
    (X :: List[Nothing]) : List[X]

Type-Invariant Containers

  1. abstract class Animal { def name: String }
  2. case class Bird(name: String) extends Animal
  3. case class Cat(name: String) extends Animal
  4. case class Box[X](var content: X)
  • Create concrete boxes

    1. val birdBox = Box(Bird("Feathers"))
    2. val catBox = Box(Cat("Fluffy"))
  • Create an abstract box

    1. var animalBox = Box[Animal](Bird("Feathers"))
    2. animalBox.content = Cat("Fluffy")
  • Alias boxes
    1. animalBox = birdBox // does not compile

Why not animalBox = birdBox?

  • Does not compile because Box[Bird] is not a subtype of Box[Animal]
  • Box is invariant in its type parameter X

Type-Invariant Containers

What would go wrong if we allow assignment?

  1. val birdBox = Box(Bird("Feathers"))
  2. var animalBox: Box[Animal] = birdBox // does not compile
    1. val animal = animalBox.content // ok, we can read bird as an animal
    1. animalBox.content = Cat("Fluffy") // we can put a cat into an animal box
    1. val bird: Bird = birdBox.content // but now the bird box contains a cat!
  • Writing to aliased container would create type violations!

Type-Covariant Containers

What if we disallow modifying boxes?

  1. case class ImmutableBox[+X](val content: X)
  • Create and alias boxes

    1. val birdBox = ImmutableBox(Bird("Feathers"))
    2. val animalBox: ImmutableBox[Animal] = birdBox // compiles
    3. val animal = animalBox.content // reading ok
    4. animalBox.content = Cat("Fluffy") // immutable, cannot write
  • ImmutableBox is covariant in X

  • Covariance type annotation +X can only be applied to read-only structures

Scala Lists are Covariant

What is the practical relevance?

  1. def printAnimalNames(animals: List[Animal]) =
  2. for a <- animals do println(a.name)
  • Print some concrete lists

    1. val birds: List[Bird] = List(Bird("Feathers"), Bird("Chirpy"))
    2. val cats: List[Cat] = List(Cat("Fluffy"), Cat("Whiskers"))
    3. printAnimalNames(birds)
    4. printAnimalNames(cats)
  • If List was not covariant, printAnimalNames with concrete arguments of type List[Bird] and List[Cat] would not compile!

Scala Arrays are Invariant

  1. def printAnimalNames(animals: Array[Animal]) =
  2. for a <- animals do println(a.name)
  3. val birds: Array[Bird] = Array (Bird("Feathers"), Bird("Chirpy"))
  4. printAnimalNames(birds)
  • Compile error

    1. printAnimalNames(birds)
    2. ^^^^^
    3. Found: (birds : Array[Bird])
    4. Required: Array[Animal]
  • Scala arrays are invariant, but Java arrays are covariant

Covariant Wildcard

How can we still print arrays generically?

  • Use a covariant wildcard to make array read-only

    1. def printAnimalNames(animals: Array[? <: Animal]) =
    2. for a <- animals do println(a.name)
    3. val birds: Array[Bird] = Array (Bird("Feathers"), Bird("Chirpy"))
    4. printAnimalNames(birds) // compiles
  • No way to write to Array[? <: Animal]

    1. animals(0) = Cat("Fluffy")
    2. ^^^^^^^^^^^^^
    3. Found: Cat
    4. Required: animals.T

What about Writing?

  1. def putCat(box: Box[Cat]) =
  2. box.content = Cat("Fluffy")
  • Unable to pass in a box of animal

    1. val animalBox: Box[Animal] = Box(Cat("Whiskers"))
    2. putCat(animalBox)
    3. ^^^^^^^^^
    4. Found: (animalBox : Box[Animal])
    5. Required: Box[Cat]
  • Because the animal box could be a bird box

    1. val animalBox: Box[Animal] = Box(Bird("Feathers"))
    2. putCat(animalBox)

Contravariance

What if we disallow reading?

  1. class MutableBox[-X](content: X):
  2. private var x: X = content
  3. def put(elem: X) : Unit = x = elem
  • Create and alias boxes

    1. def putCat(box: MutableBox[Cat]) = box.put(Cat("Fluffy"))
    2. val animalBox: MutableBox[Animal] = new MutableBox(Cat("Whiskers"))
  • MutableBox is contravariant in X

  • Contravariance type annotation -X can only be applied to write-only structures

Contravariance allows Degraded Read

  1. class MutableBox[-X](content: X):
  2. private var x: X = content
  3. def put(elem: X) : Unit = x = elem
  4. def get : Any = x
  5. def putCat(box: MutableBox[Cat]) = box.put(Cat("Fluffy"))
  • Create box and modify it
    1. val animalBox: MutableBox[Animal] = new MutableBox(Bird("Feathers"))
    2. var animal: Any = animalBox.get
    3. putCat(animalBox)
    4. animal = animalBox.get

Contravariant Wildcard

  1. def addFluffy(cats: List[Cat]) =
  2. Cat("Fluffy") :: cats
  • Add to a list of cats

    1. val cats: List[Cat] = List(Cat("Whiskers"))
    2. val moreCats: List[Cat] = addFluffy(cats) // ok
  • Add to a list of animals

    1. val animals: List[Animal] = List(Bird("Feathers"), Cat("Whiskers"))
    2. addFluffy(animals)
    3. ^^^^^^^
    4. Found: (animals : List[Animal])
    5. Required: List[Cat]

Contravariant Wildcard

  1. def addFluffy(cats: List[? >: Cat]) =
  2. Cat("Fluffy") :: cats
  • Contravariant wildcard ? >: X

  • Add to a list of animals

    1. val animals: List[Animal] = List(Bird("Feathers"), Cat("Whiskers"))
    2. val more: List[Any] = addFluffy(animals)
  • Can we do better than List[Any]?

Contravariant Type Parameter

  • Use a contravariant type parameter rather than a wildcard
  1. def addFluffy[X >: Cat](cats: List[X]) =
  2. Cat("Fluffy") :: cats
  • Add to a list of animals
    1. val animals: List[Animal] = List(Bird("Feathers"), Cat("Whiskers"))
    2. val moreAnimals: List[Animal] = addFluffy(animals)

Variance Annotations Example

  1. trait Source[+X] { def get () : X } // Covariant: read-only
  2. trait Sink [-X] { def put (x:X) : Unit } // Contravariant: write-only
  1. class Ref [ X] (var contents:X) // Invariant: read-write
  2. extends Source[X] with Sink[X]:
  3. def get () = contents
  4. def put (x:X) = contents = x
  • Create aliases
    1. val ref: Ref[Bird] = Ref(Bird("Feathers"))
    2. val src: Source[Animal] = ref
    3. val snk: Sink[Duck] = ref
  • Write to snk, read from ref and src
    1. val d = Duck("Quack")
    2. snk.put(d)
    3. val r = ref.get()
    4. val s = src.get()
  • What are the static and dynamic types of d, r, and s?

Variance Annotations Example

  1. trait Function[-X,+Y] { def apply(x:X) : Y } // Contravariant in X, covariant in Y
  • Covariant use

    1. // List[Int] <: Seq[Int], so Function[Int,List[Int]] <: Function[Int,Seq[Int]]
    2. val increment: Function[Seq[Int],Seq[Int]] = new Function[Seq[Int],List[Int]]:
    3. def apply(x: Seq[Int]) : List[Int] = x.map(_ + 1).toList
    4. // Int <: Any, so Function[Seq[Int],Int] <: Function[Seq[Int],Any]
    5. val length: Function[Seq[Int],Any] = new Function[Seq[Int],Int]:
    6. def apply(x: Seq[Int]) : Int = x.length
  • Contravariant use

    1. def chain(f: Function[List[Int],Seq[Int]], g: Function[Seq[Int],Any]) =
    2. (x:List[Int]) => g(f(x))
    3. chain(increment, length)

Summary

  • Subtype polymorphism: B<:A (B is a subtype of A)
  • Parametric polymorphism: T[X] parameterize class T with type parameter X
  • Covariant, contravariant, and invariant parametric types for B<:A
    Covariant Contravariant Invariant
    Relationship T[B]<:T[A] T[A]<:T[B] T[A]<>T[B]
    Collection List[X] Array[X]
    Annotations T[+X] T[-X] T[X]
    Functions Unit=>X X=>Unit X=>X
    Readable Writable Read-write

* Ok, because `List[B] <: List[A]` * `List` is _covariant_ * If `B<:A` then `List[B]<:List[A]` * Generally, type constructor `T[-]` is _covariant_ if `B<:A` implies `T[B]<:T[A]`

# <span class="fa-stack"><i class="fa-solid fa-circle fa-stack-2x"></i><i class="fa-solid fa-book fa-stack-1x fa-inverse"></i></span> Java Arrays are Covariant ```java public static void main (String[] args) { B[] bs = new B[] { new B (), new B () }; A[] as = bs; // OK, because covariant as[0] = new A (); // ArrayStoreException bs[0].g(); } ``` ```sh $ javac Driver.java $ java Driver Exception in thread "main" java.lang.ArrayStoreException: A at Driver.main(Driver.java:5) ``` * Every assignment to object array dynamically checked! ---

# <span class="fa-stack"><i class="fa-solid fa-circle fa-stack-2x"></i><i class="fa-solid fa-book fa-stack-1x fa-inverse"></i></span> Why? - For example, to sort ```java static void sort(Object[] xs) { ... } String[] ss = ...; sort(ss); // requires covariance ``` * With Java Generics ```java static <X extends Comparable<? super X>> void sort(X[] xs) { ... } ``` * In Scala ```scala def sort[X <: Comparable[? >: X]] (Array[X] xs) = ... ``` * Use ```java class A implements Comparable<A>{} class B extends A {} B[] bs = ...; sort(bs); // B's are comparable as A's ``` ---

# <span class="fa-stack"><i class="fa-solid fa-circle fa-stack-2x"></i><i class="fa-solid fa-book fa-stack-1x fa-inverse"></i></span> Three Types ```scala class A // Animal class B extends A // Bird class D extends B // Duck ``` ---

# <span class="fa-stack"><i class="fa-solid fa-circle fa-stack-2x"></i><i class="fa-solid fa-book fa-stack-1x fa-inverse"></i></span> Variance Annotations ```scala trait Producer[+Y] { def apply () : Y } // Covariant trait Consumer[-X] { def apply (x:X) : Unit } // Contravariant trait Function[-X,+Y] { def apply (x:X) : Y } // Both trait Operator[X] { def apply (x:X) : X } // Invariant ``` ```scala trait Producer[-Y] { def apply () : Y } ^ error: contravariant type Y occurs in covariant position ``` ```scala trait Consumer[+X] { def apply (x:X) : Unit } ^ error: covariant type X occurs in contravariant position ``` ---

- Outside the scope of this course - Bounded polymorphism (Java, C#, Scala, Flow) - Java's use-site variance and wildcards - Adhoc polymorphism, typeclasses, implicit params - Check out Scala and Typescript