Co- and Contravariance in Kotlin From an Introductionary Domain Level Perspective

Covariance and contravariance are properties of type systems of programming languages. They can be pretty intimidating words for a software developer who just wants to implement user requirements. In this article I approach the meaning of these two properties in Kotlin from a domain level perspective. It was part of an internal small workshop on Kotlin.

When you develop software you build an abstraction of a specific domain of the real world. Your data structures corresponds to real things out there, such as order items, and your procedures corresponds to real processes, such as cancelling an order. Types are important to model the elements of your domain. A class OrderItem is the abstraction of an order item. The richer the type system is, the more usable are types to model real life. The better the model is, the more errors are detected at compile time. To conclude, a richer type system leads to less error-prone software.

No Covariance in Java

Covariance is a type property which is not supported by Java. Let us say that our domain is the wild life. We have animals in general and dogs in particular and we would like to display the animals. This looks like an appropriate abstraction in Java:

public class Animal { }
public class Dog extends Animal { ... }
 
public class AnimalPrinter {
    public static void print(Collection<Animal> animals) {
        for (Animal animal : animals) {
            System.out.println("animal = " + animal);
        }
    }
 
    public static void main(String[] args) {
        List<Dog> dogs = 
          asList(new Dog("Pluto"), new Dog("Hasso"));
        print(dogs); // type error
    }
}

Unfortunately this does not work. In Java a List<Dog> has nothing to do with a List<Animal>. What you have to do, is to manually adapt the type of the list:

public static void main(String[] args) {
   List<Animal> dogsAsAnimals = 
      asList(new Dog("Pluto"), new Dog("Hasso"));
   print(dogsAsAnimals);
 }

This feels strange. What you would like to express with your types, would be something like, if I am able to work on a list of animals then I am also able to work on a list of dogs because dogs are animals.

In addition, using the Java solution I lose the information that the list of dogs consists only of dogs. This is due to the use of the type List<Animal> . This means, that firstly I can later add cats and crocodiles and secondly I can not use it with methods that insist of working only on dogs. This is one of the reasons why wild type casting is still often seen in Java.

But in Kotlin, there is

Fortunately in Kotlin, this is no problem:

open class Animal();
 
class Dog(val name:String) : Animal() {
    override fun toString(): String {
        return "Dog ${name}";
    }
}
 
fun printAnimals(animals:Collection<Animal>) {
    for (animal in animals) {
        println(animal);
    }
}
 
fun main() {
    val dogs: List<Dog> = 
      listOf(Dog("Pluto"), Dog("Hasso"))
    printAnimals(dogs);
}

In Kotlin, read-only collections by default has the property mentioned above. If you can handle animals, you can handle dogs. In type theory this means, that, if Dog is a subclass of Animal, then Collection<Dog> is a subclass of Collection<Animal>. As diagram:

Covariance with animals and dogs

In contrast to Java the variable dogs is still a list of dogs and can be used by other functions that only works on dogs and their subtypes.

The Problem with Mutability

This convenient property of covariance only holds for immutable collections. The reason is easy to understand if you look at an example, adding cats to the scene:

class Cat() : Animal() {}
 
fun addSomeCat(animals:MutableCollection<Animal>) {
    animals.add(Cat())
}
 
fun main() {
    val mutableDogs: MutableList<Dog> = 
      mutableListOf(Dog("Pluto"), Dog("Hasso"))
    addSomeCat(mutableDogs) // Type mismatch
}

Cats are also animals. I define a method, that adds a cat to a collection of animals. Now I build a mutable list of dogs, which are also animals, and I want to add a cat. And, the compiler complains and stops working.

This is because the type MutableList<Dog> would be not true anymore after adding a cat. Thus, MutableList<Dog> is not a subclass of MutableList<Animal> and MutableList is not a covariant type.

Contravariance

Covariance is pretty helpful, but sometimes your abstraction of your domain needs exactly the opposite: contravariance. Let us do an example and introduce a predator:

class Predator<in T> {
    // "in" specifies contravariance
    fun feedOn(prey: T) {}
}

A predator is defined by its prey it feeds on. If it is able to feed on dogs it can digest dalmatians and pekingese but not cats or snakes. This property is specified using the in keyword on the type variable T. Introducing two classical alien predators ALF and The Blob you can now safely feed them with cats:

fun feedPredatorWithCats(cats: Collection<Cat>,
                         predator: Predator<Cat> ) {
    for (cat in cats) {
        predator.feedOn(cat)
    }
}
 
fun main() {
    val theBlob = Predator<Animal>()
    val alf = Predator<Cat>()
    val cats = setOf(Cat(), Cat())
    feedPredatorWithCats(cats, alf)
    feedPredatorWithCats(cats, theBlob)
}

Looking at the types this means that a predator of an animal is a subclass of a predator of cats, because it can be used instead. As diagram:

Contravariance for predators

Here you can see why it is called contravariant. It inverses the order of inheritance whereas covariance preserve the order.

To sum it up

This was a quick overview why co- and contravariance matters, even if you just want to implement user requirements. There is, of course, more to these variances and how they are expressed in Kotlin. For a deeper dive into this topic, have a look at this section of the Kotlin language guide: Generics.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.