Scala Generics II: Covariance and Contravariance

In the previous article we looked at Scala Type bounds, today we will continue with Scala generics and talk about Covariance and Contravariance in generics.

Liskov substitution principle (the L. of S.O.L.I.D.) specifies that, in a relation of inheritance, a type defined as supertype must be in a context that in any case, it allows to substitute it by any of its derived classes.

In the generic types, the variance is the correlation between the inheritance relation of the abstract types and how it is “transmitted” to the inheritance in the generic classes. In other words, given a class Thing [A], if A inherits from B (A <: B), then Thing [A] <: Thing [B]? The variance models this correlation and allows us to create more reusable generic classes. There are four types of variance: covariance, contravariance, invariance and bivariance, three of which are expressed in Scala:

> Invariance: class Parking[A]

A generic class invariant over its abstract type can only receive a parameter type of exactly that type.

The error makes it clear, although Car <: Vehicle, due to that Parking is invariant over A, Parking [Car] !<: Parking [Vehicle]. However, once the abstract type is defined, it can be used freely within the class, applying the Liskov substitution principle:

The covariance allows you to type p1 as Parking[Vehicle] and assign it a Parking[Car]. But do not forget that although p1 is typed as Parking[Vehicle], it is actually a Parking[Car], this can be confusing, but below I explain that they are the covariant and contravariant positions and you will eventually get it.

Summing up:

For Parking[+A] If Car <: Vehicle Then Parking[Car] <: Parking[Vehicle]

> Contravariance: class Parking[-A]

A generic class contravariant over its abstract type can receive a parameter type of that type or supertypes of that type.

What happened? We have typed the input parameter of the add () method as A (a covariant type because of Pets [+ A]).

The compiler has complained, it says that the add input parameter is in a contravariant position, it says that A is not valid. But why?

Because if I do:

val pets: Pets[Animal] = Pets[Cat](new Cat)

Although pets are typed as Pets [Animal], it is actually a Pets [Cat], therefore, because pets are typed as Pets [Animal], pets.add () will accept Animal or any subtype of Animal. But this does not make sense, since in fact pets is a Pets [Cat] and add () can only accept Cats or Cat subtype. The compiler prevents us from falling into the absurdity of calling pets.add (Dog ()), since it is a set of Cat.

What happened? We have typed the input parameter as A (which is contravariant because Pets[-A]) and the compiler has told us that this parameter is in a covariant position, that we can not type it as A. But why?

Because if I do:

val pets: Pets[Cat] = Pets[Animal](new Animal)

The compiler would expect pets.pet to be Cat, an object able to do pets.pet.meow(), but pets.pet is not Cat, it is an Animal. And although Pets[-A] is contravariant over A, the value pet: A it’s not, once its type is defined (pets val: Pets [Cat] implies that pets.pet will be Cat), this type is definitive. If Pets were covariant over A (Pets [+ A]) this would not happen, because if we do: val pets: Pets [Animal] = Pets [Cat] (new Cat) the compiler would wait for pets.pet to be Animal, and because Cat <: Animal, it is.

Another example,

abstract class Pets[-A] {
def show(): A
}
<console>:8: error: contravariant type A occurs in covariant position in type ()A of method show
def show(): A

For the same reason as before, the compiler says that the return type of a method is in a covariant position, A is contravariant.

If I do:

val pets: Pets[Cat] = Pets[Animal](new Animal)

I would expect to be able to make pets.show.meow (), since pets it’s a Pets[Cat], show() will return a Cat. But as we’ve discovered before, it’s actually a Pets[Animal] and show() will return an Animal.

Finally I would like to show the definition of Function1 (function that accepts one input parameter) in scala:

trait Function1[-T, +R] extends AnyRef {
def apply(v1: T): R
}

When creating a function, we are already informed that it is of type Function1: If we have the classes: Class Animal, Class Dog extends Animal, Class Bulldog extends Dog and Class Cat extends Animal

In the position of T (Bulldog) we can type the input parameter as any Bulldog supertype, since Bulldog will always comply with the inheritance (Bulldog is Dog, and is also an Animal). It’s Liskov again.

And this is all for now in terms of covariance, contravariance and invariance! In the upcoming article, Scala Generics III: The Type constraints we will immerse ourselves in the world of type constraints and we will finish the introduction to Scala generics!

If you found this article about covariance and contravariance in generics interesting, you might like…