CSC447

Concepts of Programming Languages

Subtyping

Instructor: James Riely

Checked casts (from lecture on safety)

              
class A { int x; }
class B extends A { float y; }
class C extends A { char c; }

void f (B b) {
  A a = b;       // upcast always safe
}
void g (A a) {
  B b = (B) a;   // downcast must be checked
}
f (new B()); // OK
g (new C()); // ClassCastException
          

Subtyping


A x = new A ();
B y = new B ();
x = y; // B ok when A expected
          

void aConsumer (A x) { ... }
aConsumer (y);    // B ok when A expected
          

B bProducer () { ... }
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

  • Subtyping relation <: is a partial order on types
    • reflexive
      • X<:X
    • transitive
      • If Duck<:Bird
      • And Bird<:Animal
      • Then Duck<:Animal
    • ...

Subtyping Order - Top

  • Some PLs have a Top type
    • X<:Top (greater than all other types)
  • In Java - java.lang.Object above reference types
  • In Scala - scala.Any above all types
  • In Scala - scala.AnyRef above reference types

import java.io.FileInputStream

val xs:List[AnyRef] = List ("hello", FileInputStream ("a.txt"))
val ys:List[Any]    = List ("hello", 1)
          

Subtyping Order - Bottom

  • Most PLs do not have a Bottom type
    • Bottom<:X (less than all other types)
  • Recall Scala type hierarchy
  • In Scala - Bottom is scala.Nothing
  • Important for typing uses of Nil
    • Nil:List[Nothing]
    • List[Nothing]<:List[X]

val mynil1:List[Int]     = Nil
val    xs1:List[Any]     = "hello"::mynil1 // Best type possible

val mynil2:List[Nothing] = Nil
val    xs2:List[String]  = "hello"::mynil2 // Best type possible
          

Scala Lists


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:List[A] = List (A(), B()) // OK, because B <: A
var bs:List[B] = List (B(), B()) // OK
          

as = bs    
as(1).f()  
          

// mutated as --- OK, because List[B] <: List[A]
val res4: Int = 2
          
  • 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]

Scala Arrays


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:Array[A] = Array (A(), B()) // OK
var bs:Array[B] = Array (B(), B()) // OK
          

as = bs
          

     ^
error: type mismatch;
       found   : Array[B]
       required: Array[A]
  Note: B <: A, but class Array is invariant in type T.
  You may wish to investigate a wildcard type such as `_ <: A`
          
  • Why?

Invariance


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:Array[A] = Array (A(), B()) // OK
var bs:Array[B] = Array (B(), B()) // OK
          

as = bs        // ERROR, because Array[B] NOT <: Array[A]
as(0) = A()  // OK, because as:Array[A]
bs(0).g()      // Unsafe access
          
  • Covariance only safe for read only structure

Wildcard?


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:Array[_ <: A] = Array (A(), B()) // OK
var bs:Array[B]      = Array (B(), B()) // OK
          

as = bs         // OK, because Array[B] <: Array[_ <: A]
as(0) = A()  
          

        ^
error: type mismatch;
       found   : A
       required: _$1 where type _$1 <: A
          
  • Covariance only safe for read only structure
  • No way to write an array of type Array[_ <: A]

Wildcard?


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:Array[A]      = Array (A(), B()) // OK
var bs:Array[_ >: B] = Array (B(), B()) // OK
          

bs = as        // OK, because Array[A] <: Array[_ >: B]
bs(0) = B()  // OK
bs(0).g()      // Unsafe access
          

      ^
error: value g is not a member of _$1
|
          
  • Contravariance safe for write only structure
  • Degrade the type of read

Wildcard?


class A {  def f () = 1 }
class B extends A:
  override def f () = 2
           def g () = 3

var as:Array[A]      = Array (A(), B()) // OK
var bs:Array[_ >: B] = Array (B(), B()) // OK
          

bs = as        // OK, because Array[A] <: Array[_ >: B]
bs(0) = B()  // OK
bs(0)          // OK
          

val res1: Any = B@d271a54
|
|
          
  • Contravariance safe for write only structure
  • Degrade the type of read

Java Arrays Covariant


public class Driver {
  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(); 
  }
}
          

$ javac Driver.java 
$ java Driver
Exception in thread "main" java.lang.ArrayStoreException: A
  at Driver.main(Driver.java:5)
          
  • Java arrays are covariant
  • Every assignment to object array dynamically checked!

Why?

  • For example, to sort

static void sort(Object[] xs) { ... }
String[] ss = ...;
sort(ss); // requires covariance
            

static <X extends Comparable<? super X>> void sort(X[] xs) { ... }
          

def sort[X <: Comparable[_ >: X]] (Array[X] xs) = ... 

class A implements Comparable<A>{}
class B extends A {}
B[] bs = ...;
sort(bs); // B's are comparable as A's
          

Three types


class A {}           // Animal
class B extends A {} // Bird
class D extends B {} // Duck
          

Variance annotations


trait Source[+X] { def get    () : X    } // Covariant 
trait Sink  [-X] { def put (x:X) : Unit } // Contravariant
class Ref   [ X] (var contents:X)         // Invariant
  extends Source[X] with Sink[X] {
    def get ()    = contents
    def put (x:X) = contents = x
}
          

val ref : Ref   [B] = Ref[B] (B())
val src : Source[A] = ref
val snk : Sink  [D] = ref
          

val d = D()
snk.put(d)
val r = ref.get()
val s = src.get()
          

val d: D = D@595713f3
val r: B = D@595713f3
val s: A = D@595713f3
          

Variance annotations


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
          

trait Producer[-Y]    { def apply ()    : Y    }
            

                            ^
error: contravariant type Y occurs in covariant position
            

trait Consumer[+X]    { def apply (x:X) : Unit }
            

                                   ^
error: covariant type X occurs in contravariant position
            

Variance when B<:A

Covariant Contravariant Invariant
Relationship T[B]<:T[A] T[A]<:T[B]
Collection List[X] Array[X]
Read only Write only Read / Write
Reference Source[X] Sink[X] Ref[X]
Producer Consumer Operator
Function Unit=>X X=>Unit X=>X

Here's another example

Polymorphism

  • parametric polymorphism
  • subtype polymorphism
  • 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