Covariance and Contravariance in Generics

September 07, 2018 6 minutes

Yesterday I was supposed to finish my notes on the Advanced Kotlin course, but then I got to that part in any book or course that exists when the language supports generics. You know which part. Hint in post title.

Context

open class Person
class Astronaut: Person()

You can imagine they have whatever properties you wish. The main point is that Astronaut derives from Person. With that said, the following should work:

class Container<T>(val content: T)

fun printContents(container: Container<Person>) {
    println(container.content)
}

fun main(args: Array<String>) {
    val astronautContainer = Container(Astronaut())
    printContents(astronautContainer)
}

But it doesn’t.

Error:(13, 19) Kotlin: Type mismatch: inferred type is Container<Astronaut> but Container<Person> was expected

You see, by default, things work like this:

  • Astronaut can be used in place of Person, due to polymorphism.
  • Container<Astronaut> cannot be used in place of Container<Person>, because it’s potentially dangerous.

It’s dangerous because of cases like the following:

val alexey = Astronaut()
var personc: Container<Person> = Container<Astronaut>(alexey)

val alexis = Person()
personc.content = alexis

The contents of the Container<Astronaut> have been modified, and now is a Person. A more realistic example would be the addition of instances of a base class to a mutable list of a derived class. Our example is simplified.

Now… an observant reader would notice that our “dangerous” example wouldn’t even compile, as we defined the contents of the Container<T> as readonly (using the val keyword), so silly shenanigans like that couldn’t ever happen.

Covariance

As we (and the compiler) know that we aren’t going to mutate Container<T> instances, we can add the out modifier to our generic type declaration, leaving it like this:

class Container<out T>(val item: T)

Our example, composed of the first two code samples together, will now compile.

Let’s define more formally what’s covariance then:

Covariance is enabling the compiler to treat Structure<DerivedType> as if it were a subclass of Structure<BaseType>. Covariance for a generic type parameter can only be enabled if it’s only used for returns and/or immutable properties.

This of course is far from an academic explanation of covariance, but it better reflects what happens in practice. If we wish to be more specific: returns and immutable properties are described as out positions by the Kotlin compiler. You can mark a type parameter as covariant if it’s only used in out positions.

Contravariance

Covariance makes sense, no? After all, if we have something that requires a Structure<Person>, then it’s logical that it should take a Structure<Astronaut>, as Astronaut is a Person, so every operation that could be done in a Person is also available in Astronaut.

Contravariance is the opposite of that. It’s about using a Structure<Person> where a Structure<Astronaut> is expected. It throws out of the window any intuition we got from our existing OOP studies. We clutch our books filled with cute animal taxonomies, we pray to Craig Larman (but hear no response), and we finally surrender to our fear of the unknown.

It makes no sense!, you say in anger. How is that can we use a Person in place of Astronaut?.

The first step is stop thinking of it as if Person subclassed Astronaut, because that makes no sense, and we both know it. Variances aren’t about the type parameters themselves, but about the structures that have those type parameters.

As we said, contravariance is about how Structure<Person> can be used in place of Structure<Astronaut> if Structure is defined as Structure<in T>. Person cannot be used in place of Astronaut by itself.

Study the following example:

// https://kotlinlang.org/docs/reference/generics.html

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

Let’s walk through it together, step by step

Comparable<in T> only consumes T, using it as a parameter for it’s only member function compareTo.

It is declared contravariant, noted by the in keyword. This is only posible because it doesn’t produce T. Trying to declare, let’s say, a multiplyByTwo(): T function would cause a compilation error.

We’ll cover why this is relevant later on.

Double is a Number

Any operation that can be done in a Number can also be done in a Double.

The function compareTo(other: Number) can also be called with a Double, and in fact that’s what’s done in x.compareTo(1.0).

Basic polymorphism, nothing new here.

Therefore, Comparable<Number> can be used in any place where a Comparable<Double> is required.

And here’s the fucker.

Comparable<Number> is able to treat Doubles as Numbers as the former derives from the latter, and therefore whenever we required a Comparable<Double> we could just pass a Comparable<Number> to do the job. Therefore contravariance. QED.

But why can’t we return T things in a contravariant data type?

Let’s suppose we were able to have the following (we can’t; it doesn’t even compile):

class MutableList<in T> {
    fun add(entity: T): Unit
    fun getAll(): List<T>
}

Then, in a stroke of genius:

val personList = MutableList<Person>()
personList.add(alexis)
val astronautList: MutableList<Astronaut>() = personList // contravariance OK
val astronauts: List<Astronaut> = astronautList.getAll() // oopsie woopsie!

You can’t ensure that getAll will be able to return instances of the derived type.

In fact, there’s no reason to think that MutableList<Person>.getAll() would return List<Astronaut>. Even if for some reason it actually returned instances of Astronaut, they’re still typed Person due to the return type. You would need to do some UGLY upcasting in that case for you to be able to use them in a context that requires Astronauts. AND EVEN THEN, there’s no runtime guarantee that we just didn’t add a person instance somewhere like we did in our example.

So that’s why you can’t return anything of a contravariant generic type.

Summary

Covariance is telling the compiler that it can use Structure<DerivedType> wherever Structure<BaseType> is expected by adding the out modifier to a generic type parameter. For this to be possible, covariant generic type parameters cannot be used in in positions in the class or interface definition. Example in positions are as function parameters, or as properties with a setter (i.e. mutable).

Contravariance is telling the compiler that it can use Structure<BaseType> wherever Structure<DerivedType> is expected by adding the in modifier to a generic type parameter. For this to be possible, contarvariant generic type parameters cannot be used in out positions in the class or interface definition. Example out positions are as return values, or as properties with a getter.