Variance is the correlation of subtyping relationships of complex types and the subtyping relationships of their component types.

Note: Examples taken in this section include following classes – Node/ Empty/ NotEmpty
Each Node contains a value and and reference to next Node.  It if of two type -> Empty and NonEmpty where hierarchy in increasing order are Empty -> Node -> AnyRef -> Any, and similarly NonEmpty -> Node -> AnyRef -> Any

 

Bounds

  • S <: T

    • means S is a subtype of T
    • i.e., T is an upper bound for S
    • e.g [S <: Node], which means S can only be instantiated to types that confirm to Node
  • S >: T

    • means T is subtype of S
    • i.e, T is an lower bound of S
    • e.g. [S >: NonEmpty], which means S can be NonEmpty, Node, AnyRef, Any

e.g if we want to write a method ‘assertAllPositive‘ that can
– take Empty and return Empty.
– take NonEmpty and return either NonEmpty Or throw Exception if all not positive.

This can be written as,

def assertAllPositive[S <: Node](input: S): S = ...

[S >: NonEmpty <: Node], would restrict S to any types between Node and NonEmpty

 

Variance

Variance is the correlation of subtyping relationships of complex types and the subtyping relationships of their component types.

The variance checks are automatically performed by Scala compiler for us.

Covariance

If there is a parameterized type C[T], and there are types A, B where A <: B. Then C[T] is  covariant, if C[A] <: C[B].
A type parameter T of a generic class can be made covariant by using the annotation +T. e.g C[+T]. Covariance types can only appear

  • in method Result types
  • or as lower bounds on method type parameters.

Example: Lists are co-variant in Scala.
The Scala standard library has a generic immutable sealed abstract class List[+A] class, where the type parameter A is covariant.

Consider following example

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

This means that a List[Cat] is a List[Animal]and a List[Dog] is also a List[Animal].
Intuitively, it makes sense that a list of cats and a list of dogs are each lists of animals, and you should be able to substitute either of them for a List[Animal]

object CovarianceTest extends App {
  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"))

  printAnimalNames(cats)
  // Whiskers
  // Tom

  printAnimalNames(dogs)
  // Fido
  // Rex
}

 

Contravariance

If there is a parameterized type C[T], and there are types A, B where A <: B. Then C[T] is  contravariant, if C[A] >: C[B].
A type parameter T of a generic class can be made contravariant by using the annotation -T. e.g C[-T].
That is, for some class Writer[-T], making T contravariant implies that for two types A and B where A is a subtype of B, Writer[B] is a subtype of Writer[A].
Contravariant types can only appear

  • can only appear in method parameters.
  • can also appear as upper bounds of method type parameters.

Consider the following class heirarchy

trait Item
class PlasticItem extends Item
class PaperItem extends Item
class PlasticBottle extends PlasticItem
class NewsPaper extends PaperItem

and following methods

class GarbageCan[-A] {
}

def setGarbageCan(gc: GarbageCan[PlasticItem]): Unit = {
  // sets garbage can for PlasticItem items
}

Now, the setGarbageCan method can take argument of type GarbageCan[Item] and GarbageCan[PlasticItem], but not GarbageCan[PlasticBottle]

 

// contravariant subtyping
setGarbageCanForPlastic(new GarbageCan[Item])

// invariant
setGarbageCanForPlastic(new GarbageCan[PlasticItem])

// Compile error ! covariant subtyping
setGarbageCanForPlastic(new GarbageCan[PlasticBottle])

 

InVariance

Generic classes in Scala are invariant by default.
This means that they are neither covariant nor contravariant.
Invariant types are denoted by C[T], and they can appear anywhere in functions.

trait Node[T]
class Empty[T] extends Node[T]
class NonEmpty[T] extends Node[T]

 

Variance Relations between functions

If we have types A1, A2, B1, B2, then a function of type A1 => B1 <: A2 => B2
1. if method arguments are contravariant, i.e., A2 <: A1
2. if result types are covariant, i.e., B1 <: B2

So a function A => B, can be expressed in scala as

trait Function1[-A , +B] { 
  def apply(x: A]: B 
}

 

Variance in Arrays – Java vs Scala

Arrays are variant in Java and non Variant in Scala.

Consider the java example below

NonEmpty[] a1 = new NonEmpty[]{ new nonEmpty(1, new Empty()) };
Node[] b = a1;
b[0] = new Empty();
NonEmpty a2 = a1[0];

Above code will compile in Java because, arrays are covariant in java.
But arrays in java store type information, so when b[0] is set to Empty, it results in ArrayStoreException at runtime because it can only take NonEmpty types.

Consider similar example in Scala

a1: Array[NonEmpty] = new NonEmpty[] { new nonEmpty(1, Empty) } 
b: Array[Node] = a 
b(0) = Empty 
a2: NonEmpty = a1(0)

In Scala above code will not compile, because Arrays are not covariant in scala. So an array of type Array[NonEmpty] cannot be assigned to Array[Node].

 

Reference

  1. https://www.coursera.org/learn/progfun1/home/welcome
  2. https://docs.scala-lang.org/tour/variances.html
  3. http://blog.kamkor.me/Covariance-And-Contravariance-In-Scala/