Introduction
Generic classes can be made covariant or contravariant. An example of covariant class is List
, which could be declared as the following. (Taken from chapter 3 of Functional Programming in Scala)
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
Notice the +
sign in List[+A]
, which means List
is covariant. For example, if we have Cat
and Dog
extending from Animal
, List[Dog]
and List[Cat]
will be subtypes of List[Animal]
. Also note that the object Nil
extends from List[Nothing]
, which makes it a subclass of List[Dog]
, List[Cat]
, or any other List[A]
because Nothing
is the subtype of all types.
An example of contravariance in Scala is the following Printer
examples. (Taken from Tour of Scala)
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)
}
It turns out that a Printer[Animal]
can be used wherever a Printer[Cat]
is required, as is shown below
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
As a result, Printer[Animal]
is actually a subtype of Printer[Cat]
, and that is why we
we make Printer
a contravariant generic class.
The get-put principle
The covariance and contravariance of generic classes follow the get-put principle (from the book Java Generics and Collections), which says we use covariance when we only get values from a structure, and use contravariance when we only put to it. An example in Java looks like the following.
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}
In the example above, we get values from src
with src.get()
and put
values to dest
with dest.set()
, so we use extends
for src
(covariance) and super
for dest
(contravariance).
In Scala, the get-put principle also holds. List
is immutable and we only get
values from it, so it’s made a covariant class. For Printer
it’s a little tricky. Printer
classes doesn’t store objects, but we can still think of the print
method to be kind of a put
operation since it takes a parameter of type A
. As a result, the Printer
classes is contravariant.
Notice the difference of Java and Scala generics: In Java we declare whether a generic class is covariant or contravariant when we declare variables of the class (using super
or extends
), but in Scala we do it when we define the class (using +
or -
). As a result, in Java a class can sometimes be covariant and sometimes be contravariant, like the List
class in the Java copy
example above, but in Scala, a class can only be either covariant or contravariant (or, of course, invariant) consistently.
Notice that the get-put principle works slightly differently when we are using Scala-style class-level variation instead of Java-style usage-level variation. In Scala, it means:
-
Covariant type parameters can only appear in return types and not in method parameters since we “get” return values from the objects. For example, we could define a method
head
forList
which returnsA
, but we could not define anappend
method forList
which takesA
-
Contravariant type parameters can only appear in method parameters and cannot appear in method parameters since we “put” arguments to the objects. For example, we could define the method
print
forPrint
which takesA
, but we could not define any methods which returnsA
A caveat in Java
Consider the Java code below
Integer[] ints = new Integer[]{1,2,3};
Object[] objects = ints;
objects[2] = 3.14;
Think about the following questions:
- Does the code compile? Does the code run correctly?
- At which line will an error occur?
- What is the root cause of the error? If we could redesign Java, at which line should an error occur?
Here is the answer. The code compiles but it doesn’t run correctly, a java.lang.ArrayStoreException
will be thrown at line 3. The apparent problem is that we put a double into an array of integers, but the root problem happens at line 2: We are able to assign an Integer[]
to an Object[]
(i.e. arrays are considered covariant) even when we will “put” to the array subsequently at line 3! According to the get-put principle, in this case we should not consider arrays as covariant. It turns out Java (incorrectly) thinks arrays as covariant but they do not always behave covariantly. So if we could redesign Java we should made arrays invariant (it’s hard to provide super
, extends
like syntax for arrays because arrays do not use <>
), which means line 2 should get a compile time error (because Object[]
is not a subtype of Integer[]
). In Scala the class Array
is invariant, which solves this issue in Java.
Function types
Suppose we have a function which takes an animal and returns its name. like below
def getNameAnimal(animal: Animal): String =
s"animal: $animal.name"
We have a similar function taking a Cat
.
def getNameCat(cat: Cat): String =
s"cat: $cat.name"
Is there a subtype relation between the two functions? The answer is yes. Whenever we want to use the function getNameCat
, the function getNameAnimal
could also suffice since is expects an Animal
which is a supertype of Cat
. For example, say if we have a higher order function which prints the name of a cat.
def printName(cat: Cat, getNameFn: Cat => String) =
println(getNameFn(cat))
We can definitely pass getNameCat
to the getNameFn
argument, but it’s equally good to pass getNameAnimal
, since it takes an Animal
, and it should also work well if a Cat
is passed.
val cat = Cat("kitty")
printName(cat, getNameCat) // "cat: kitty"
printName(cat, getNameAnimal) // "animal: kitty"
In other words, the type of the function getNameAnimal
(Animal => String
) is a subtype of Cat => String
.
What if we have a function taking whatever arguments and returning a Cat
or an Animal
? It turns out a => Cat
is a subtype of a => Animal
for whatever type a
, since whenever a function returning an Animal
is expected, a function returning a Cat
also works well.
Scala illustrates the subtyping relations of functions w.r.t. their parameters and return types with the definition of function types. Below is the definition of any functions taking one argument T1 => R
(reference).
trait Function1[-T1, +R]{
def apply(v1: T1): R
}
The difference between Function1
and List
(or Printer
) is that here we have more than one type parameters, T1
and R
, and their variation properties are different. A function is contravariant with respect to its argument (e.g. Animal => String < Cat => String
), and covariant with respect to its return type (e.g. String => Cat < String => Animal
). We can no longer say whether a generic class is covariant or contravariant when it has more than one type parameters. Instead, we call T1
a contravariant type since Function1
is contravariant w.r.t. it. Similarly we call R
a covariant type.
Also note that the variation property of a function type is consistent with the get-put principle. We get the return value from a function by putting the argument into the function, so the return type is covariant and the argument type is contravariant.
For functions with more than one arguments, they are contravariant w.r.t. all of their arguments and covariant w.r.t. their return type. For example, below is the definition of functions taking two arguments.
trait Function2[-T1, -T2, +R]{
def apply(v1: T1, v2: T2): R
}
Variance and methods
Let’s try to define a method prepend
for the class List
. We could define it in the companion object.
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
object List {
def prepend[A](v: A, l: List[A]): List[A] =
Cons(v, l)
}
It works well. However, how should we define prepend
to be a method of List
?
A naive definition would be the following:
sealed trait List[+A] {
def prepend(v: A): List[A] =
Cons(v, this)
}
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
However, the compiler will give you an error if you define prepend
this way:
Covariant type A occurs in contravariant position in type A of value v
We already know what is “covariant type A”: It means the generic class List
is covariant w.r.t. type parameter A
, but what is “contravariant position”? Let me explain.
List
is covariant, which means List[Cat]
is a subtype of List[Animal]
. If the naive definition passes the compiler, what will happen to the following code snippet?
val cat1 = Cat("cat1")
val cat2 = Cat("cat2")
var cats: List[Cat] = Cons(cat1, Cons(cat2, Nil))
var animals: List[Animal] = cats
val dog = Dog("dog")
val animals2 = animals.prepend(dog) // oppppps!
If we somehow redesigned the Scala compiler so that it does not give us the “contravariant position” error, now questions:
- Would the code compile? Would the code run correctly?
- At which line would an error occur?
The answer is, the code compiles correctly, but line 6 will fail. Line 6 passes the compiler since animals
is a List
of animals, whose prepend
method takes an animal, and a dog is definitely suitable. However, we know that animals
is actually a list of cats, whose prepend
method takes a Cat
instead of any arbitrary animals. In this way, line 6 should throw an exception that Dog
is invalid here.
In fact, we are running into a similar situation as the caveat in Java. We also violates the get-put principle, since we can “put” a value of type A
to a List
with the prepend
method, but List
is covariant of A
.
Contravariant position
Let’s think about how to make the method prepend
type safe. One approach is to make List
an invariant class, just like what we do for Array
. However, we have a better approach.
Let’s assume we define the type of v
in the prepend
method to be a type derived from A
, called F[A]
, where F
is called a type constructor that derives a type from another type. List[A]
is an example of a type constructor. When F
is the identity type constructor, F[A] = A
so our naive definition is actually a special case. In this way, the signature of prepend
looks like:
sealed trait List[+A] {
def prepend(v: F[A])
}
Currently we do not care what F
is, we just think of it in an abstract way. Later we will discuss how we should define F
. Here for the new definition, we supply the prepend()
method F[Animal]
when it’s called with List[Animal]
and we supply it F[Cat]
when called with List[Dog]
. For type-safety, List[Cat].prepend
has to be able to accept any possible values given to List[Animal].prepend
. If we think of types as sets (for example, the type String
corresponds to the set of all strings), it means the set corresponding to F[Animal]
is a subset of the set corresponding to F[Cat]
. In other words, F[Cat]
has to be a supertype of F[Animal]
.
List[Cat]
is a subtype of List[Animal]
, while F[Cat]
has to be a supertype of F[Animal]
. You might notice that the variance of F
goes in the contrary direction with respect to the variance of the generic class List
. Here comes the definition of contravariant position:
We say something is in contravariant position if the variance of its type has to go in the contrary direction with respect to the variance of the class.
Method parameters are something that are in contravariant positions. With this definition, the meaning of the compiler error becomes clear to us. The value v
is a parameter of method prepend
, so it’s in contravariant position, which means its variance has to go in the contrary direction w.r.t. the class List
. List
is covariant w.r.t. type A
, so the type of v
has to be contravariant w.r.t. type A
. However, we defined v
’s type to be A
, which is actually covariant w.r.t. A
. That is why we get the compiler error:
Covariant type A occurs in contravariant position in type A of value v
The solution
How to design the type constructor F
? Here is a common approach.
sealed trait List[+A] {
def prepend(v: B >: A): List[B] =
Cons(v, this)
}
Here B >: A
is our F[A]
, it means any supertypes of A
and it’s a type derived from A
. It enables Scala compiler to infer a common super type of A
and the type of the argument given to prepend
. For example, when we call prepend(dog)
, B
will be inferred as Animal
. When we call prepend(123)
, B
will be inferred to be Any
.
val cat1 = Cat("cat1")
val cat2 = Cat("cat2")
var cats: List[Cat] = Cons(cat1, Cons(cat2, Nil))
var animals: List[Animal] = cats
val dog = Dog("dog")
// animals is List[Cat], but the argument is type Dog
// this does not pose any problems since Scala infer type B to be type Animal
// and animals2 is List[Animal]
val animals2 = animals.prepend(dog)
Note that the definition of prepend
illustrates the power of immutability. prepend
does not modify the list. Instead, it returns a new list, which gives it a chance to return a list of different type.
How does B >: A
satisfies our contravariant position requirement? It turns out that the type B >: A
is actually contravariant with respect to type A
. For example, B >: Animal
means any supertypes of Animal
and B >: Cat
means any supertypes of Cat
. A supertype of Animal
is automatically a supertype of Cat
, so B >: Animal
is a subtype of B >: Cat
. Generally, B >: A1
is a subtype of B >: A2
if A1
is a supertype of A2
, or, in other words, B >: A
is contravariant with respect to type parameter A
.
Similar to contravariant position, we have something called covariant position. We say something is in covariant position if the variance of its type has to go in the same direction with respect to the variance of the class. The return value of a method is in the covariant position.
We introduced the fact that method parameters are in contravariant position using an example of covariant generic class, but actually it holds for all generic classes. Suppose we have a generic class P
with a type parameter T
, and P[A] < P[B]
. Suppose P[T].m(value: G[T])
is a method which takes a parameter of type G[T]
. For type safety, P[A].m(value: G[A])
should accept any arguments given to P[B].m(value: G[B])
, which means the set corresponding to G[B]
is a subset of the set corresponding to G[A]
, or, in other words G[B] < G[A]
. since P[A]
is defined to be a subtype of P[B]
, we can find that value
, a method parameter, is in contravariant position. Notice that we do not specify the variance of P
w.r.t. T
.
We already know that functions are contravariant w.r.t. their parameters, and now we also know that method parameters are in contravariant position. What is the relation between the two “contravariance”? It turns out that by ensuring contravariant position for value
, we have G[B] < G[A]
, making P[A].m
become a subtype of P[B].m
(because functions are contravariant w.r.t. their parameters), which ensures that P[A].m
is always suitable when P[B].m
is required, a.k.a. the “type safety” for P[A]
and P[B]
. Below is a illustration of the relations. By the same reason, the return value of a method is in the covariant position since functions are covariant w.r.t. their return value.
Let’s see a contravariant class: the Printer
type we discusses above, which has a method print
abstract class Printer[-A] {
def print(value: A): Unit
}
The parameter value
of print
is also in contravariant position. The variance of type A
(the type of value
) goes in contrary to the variance of the class, so it satisfies this requirement.
Note that the Printer
class also satisfies the get-put principle since it uses a contravariant type (A
) in method parameters. Actually we can view variance position as an extension of the get-put principle. By using contravariant type parameters (like type A
in Printer
) for method parameters, the variance of the method parameter (like value: A
) goes in contrary to the variance of the class, so it automatically satisfies the contravariant position requirement. Similarly, by using covariant type parameters for return values, it automatically satisfies the covariant position requirement.