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
- https://www.coursera.org/learn/progfun1/home/welcome
- https://docs.scala-lang.org/tour/variances.html
- http://blog.kamkor.me/Covariance-And-Contravariance-In-Scala/