Covariance and Contravariance
Some weeks ago I gave a introductory talk of Scala for Java programmers. At some point I introduced algebraical data types in Scala using sealed traits, and I though it was a nice moment to show how the language supports covariance and contravariance for generic types.
The audience didn’t agree it was a nice moment. They weren’t familiarized with these terms at all, and we had no time enough to discuss the point in depth. So I decided it was a very good topic to discuss in typeinference.com.
Any programmer is familiarized with the subtyping relationship between two types. Let’s say we have Base
and Derived
classes so Derived
inherits (or extends) Base
, as in:
The inheritance mechanism conveys a subtyping relationship of Derived
respect Base
. In other words, any instance of Derived
is also an instance of Base
. Because of that, the following statements are valid in Java.
All right. Now let’s say we have List<Base>
and List<Derived>
instead. Is there any subtyping relationship among them? Is the following code valid?
I’m sure your brain says “Sure! Why not?” but your instinct prevents you from answering. For sure, things get complicated with generics, and this kind of subtyping relationships have non trivial consequences.
Java generics are not covariant
The property of a complex type as List<Derived>
to be a subtype of List<Base>
is known as covariance. And it is not supported in Java collections. Therefore, the code above compile with errors.
You may think it is Ok to consider a collection of Derived
as subtype of a collection of Base
. After all, as they are collections, they could be seen as a subset (in algebra of sets) of elements of that type. Since List<Base>
is a subset of Base
, List<Derived>
is a subset of Derived
, and Derived
is a subset of Base
, any List<Derived>
must be also a subset of Base
, and therefore a subset of List<Base>
as well. And this rationale is correct, so covariance is natural for collections.
But, if maths say we are right, why do Java prevents us to use covariance? Because of this:
Let’s say we have OtherDerived
class that extends Base
as Derived
does. We use the bar
variable, of type List<Base>
, to insert a new element of OtherDerived
type. This is valid, since List<Base>
may contain any instance of Base
, including those of OtherDerived
type. But remember: bar
is a reference to an instance of ArrayList<Derived>
class. Any element inserted in bar
would become also an element of foo
. Inserting such a OtherDerived
instance in bar
means foo
contains elements of Derived
and OtherDerived
types. And that is contrary to the type contract of foo
(List<Derived>
).
This is a good reason for Java to consider generic types non covariant. Nevertheless primitive arrays does support covariance, making the following code valid to the compiler.
Although it compiles, it is not type safe and it would crash at runtime with an exception. The reason to support covariance for primitive arrays is likely related with the sorting functions of java.util.Arrays
. Or perhaps they didn’t realize they were type unsafe until they were implementing the JVM. Who knows?
Covariance, contravariance and functions
Covariance is closely related to functions. Actually, the problem discussed above may be seen from the perspective of add()
method and how the covariance affects subtyping.
Perhaps you didn’t think about it, but functions are also types. And, as such, they may have subtyping relationship among them. For instance, the following two functions keep a subtyping relationship.
Here bar
is a subtype of foo
. Following Liskov substitution principle, wherever a function Base -> Base
is required, you may use the function Base -> Derived
. That is natural: if the function is invoked expecting a Base
as result, receiving a Derived
instance is also valid since Derived
objects are also Base
objects.
That is respect the return value. What about function arguments? This is where we introduce a new concept very hard to explain and understand: contravariance. So please read the following lines carefully.
Let’s say we have the following two functions:
Is bar
a subtype of foo
as before? To be so, wherever we expect a function Base -> Base
we should accept a function Derived -> Base
. If we do so, we could invoke such function with an instance of Base
as input argument. But Derived -> Base
cannot accept that! It needs a Derived
instance. So indeed bar
is not a subtype of foo
.
And what about the other way around? Is foo
a subtype of bar
? Again, to be so wherever we expect a function Derived -> Base
we should accept a function Base -> Base
. And this time… that’s true!!! If we invoke such function with a Derived
instance as input argument, Base -> Base
would accept it since Derived
objects are also Base
objects. So foo
is a subtype of bar
indeed.
If we represent unary functions using generics, we might have a type like Function<T, R>
, where T
is the type of the input argument and R
is the type of the function result. According to the subtyping relationships we just have illustrated:
Function<T, Derived>
is a subtype ofFunction<T, Base>
. As we discussed before,Function<T, R>
is covariant respectR
parameter.Function<Base, R>
is a subtype ofFunction<Derived, R>
. This is exactly the opposite to covariance, so we sayFunction<T, R>
is contravariant respectT
.
All this leads to the following rule: functions are contravariant in the input type and covariant in the output type.
Covariance, contravariance and inheritance
In order to one class to extend another class, it should provide all its members to the same type. That comprises its methods. So in:
… the Bar
class should override method
to the same function type as defined in Foo
signature. The same function type, including a subtype function. So the following code is correct.
Going back to Java List<T>
discussion, we may demonstrate it is not covariant because of add()
method from the function subtyping perspective.
If we instantiate List<T>
with Base
and Derived
, we would have something equivalent to the following classes:
In order to check whether DerivedList
is a subclass of BaseList
, we must check its function. Respect get()
method, there is no problem at all. int -> Derived
is a subtype of int -> Base
, so covariance rules are not violated. Respect add()
function, Derived -> void
is not a subtype of Base -> void
, so covariance is broken.
Covariant generics and immutability
Although Java doesn’t support it, covariance is compatible with generic types. With the appropriate compiler support (as Scala compiler provides), it would be possible to specify constraints on the generic parameters to avoid situations where type integrity is broken.
The following code shows how we can create a generic class List[T]
in Scala that supports covariance.
In this code, [+T]
means the generic class is covariant respect T
, so the following code is valid:
All right. But we are cheating here. As discussed above, get()
method is not an obstacle for covariance as add()
is. If we try to add the latter we would face problems.
This code compiles with the following error:
Scala compiler knows covariant parameters cannot be used as input arguments, which are in a contravariant position. We can fix that by using a generic function instead.
Using this add()
method, the type of value
is constrained to be a supertype of T
. That’s what [U >: T]
means. Also, instead of returning Unit
(e.g., void
), we return a new list resulting from appending value
and the end of the target list. In other words, we are making our List<T>
type immutable. Why do we do that? Well, it is not easy to explain, but if we keep some storage to hold the elements of the list, this storage would hold elements of type T
. You cannot add elements of type U
to that storage. The only possible option is to return a new list instance that considers all its members as U
type. So inserting a Base
value in bar
returns a new list of Base
objects (previous elements of the list were Derived
objects, which are also of type Base
). We could even add an Object
element to that list, which produces a List<Object>
list.
Conclusions
Since we are discussing about algebra of sets, let’s make a new allegory using sets. Consider a set P
of all possible computer programs. There is a subset of P
, you call it C
, that represents the programs your compiler considers right (compile without errors). There is another subset of P
known as V
that represents the valid programs, i.e. those programs that work according to the desire of their authors.
In a perfect world, C
and V
would be exactly the same set. In other words, your compiler would compile without errors every valid program. And every program your compiler accepts would be valid and would have no errors. But this happens only in a perfect world. In the real world, your compiler considers some valid expressions (from the type system perspective) as errors. And of course, some of the programs your compiler doesn’t complain about are wrong.
Covariance and contravaciance are type system features some compilers don’t cover. This means some valid programs are not accepted by these compilers. You might think such features are barely useful, and they doesn’t worth the effort of understanding their operation. But, if you think about them in C
vs V
terms, you would realize we are increasing the size of C
, conquering new lands to V
. More and more valid programs would be accepted by your compiler. The intersection of V
and C
would be reduced. And that’s indeed a real benefit.