TP

The theory behind covariance and contravariance in C# 4

In C# 4.0, we can annotate generic type parameters with out and in annotations to specify whether they should behave covariantly or contravariantly. This is mainly useful when using already defined standard interfaces. Covariance means that you can use IEnumerable<string> in place where IEnumerable<object> is expected. Contravariance allows you to pass IComparable<object> as an argument of a method taking IComparable<string>.

So far, so good. If you already learned about covariance and contravariance in C# 4, then the above two examples are probably familiar. If you're new to the concepts, then the examples should make sense (after a bit of thinking, but I'll say more about them). However, there is still a number of questions. Is there some easy way to explain the two concepts? Why one option makes sense for some types and the other for different types? And why the hell is it called covariance and contravariance anyway?

In this blog post, I'll explain some of the mathematics that you can use to think about covariance and contravariance.

Covariance and contravariance in C#

Let's say that we have a hierarchy of classes as shown in the diagram on the right. All types are subclasses of the .NET object type. Then we have Animal which has two subclasses Cat and Fish and finally, there is also some other .NET type Random.

Covariance

Let's start by looking what covariance means. Assuming that every Animal has a Name, we can write the following function that prints names of all animals in some collection:

void PrintAnimals(IEnumerable<Animal> animals) {
  for(var animal in animals)
    Console.WriteLine(animal.Name);
}

It is important to note that IEnumerable<Animal> can be only used to read Animal values from the collection (using the Current property of IEnumerator<Animal>). There is no way we could write Animal back to the collection - it is read-only. Now, if we create a collection of cats, can we use PrintAnimals to print their names?

IEnumerable<Cat> cats = new List<Cat> { new Cat("Troublemaker") };
PrintAnimals(cats);

If you compile and run this sample, you'll see that the C# compiler accepts it and the program runs fine. When calling PrintAnimals, the compiler uses covariance to convert IEnumerable<Cat> to IEnumerable<Animal>. This is correct, because the IEnumerable interface is marked as covariant using the out annotation. When you run the program, the PrintAnimals method cannot cause anything wrong, because it can only read animals from the collection. Using a collection of cats as an argument is fine, because all cats are animals.

Contravariance

Contravariance works the other way than covariance. Let's say we have a method that creates some cats and compares them using a provided IComparer<Cat> object. In a more realistic example, the method might, for example, sort the cats:

void CompareCats(IComparer<Cat> comparer) {
  var cat1 = new Cat("Otto");
  var cat2 = new Cat("Troublemaker");
  if (comparer.Compare(cat2, cat1) > 0) 
    Console.WriteLine("Troublemaker wins!");
}

The comparer object takes cats as arguments, but it never returns a cat as the result. You could say that it is a write-only in the way in which it uses the generic type parameter. Now, thanks to contravariance, we can create a comparer that can compare animals and use it as an argument to CompareCats:

IComparator<Animal> compareAnimals = new AnimalSizeComparator();
CompareCats(compareAnimals);

The compiler accepts this code because the IComparer interface is contravariant and its generic type parameter is marked with the in annotation. When you run the program, it also makes sense. The compareAnimals object that we created knows how to compare animals and so it can certainly also compare two cats. A problem would be if we could read a Cat from IComparer<Cat> (because we couldn't get Cat from IComparer<Animal>!), but that is not possible, because IComparer is write-only.

Categories and classes

Like surprisingly many other programming language concepts, the terms covariance and contravariance come from an abstract branch of mathematics called category theory. Category theory has a reputation for being very abstract - in fact, the category theory wikipedia page mentions that some call it "general abstract nonsense". The theory starts with a fairly simple definition and then builds more and more abstractions on top of the previous layer, so it is easy to lose track. However, we do not need to go very far to explain covariance and contravariance.

Category of classes

Let's take another look at the diagram that relates different classes in our example (beginning of the previous section). We can read an arrow from A to B in the diagram as it is possible to use A where B is expected. For example, it is possible to use Cat where an Animal is expected and it is possible to use Animal where an object is expected.

To make the diagram complete, we need to add a couple of arrows (see the new version on the right).

What I described in this section is essentially what category theorists call a category - objects connected with arrows that satisfy the two properties above. Let's look at the definition more formally...

Side-note. If you want to learn even more category theory, then note that the class object is interesting, because there is exactly one arrow from every other class in the hierarchy (any type can be used where object is expected). In category theory, this is called terminal object.

Definition of category

A category is defined as a set of objects together with arrows between them. You can visualize it as a diagram with dots or boxes (representing objects) and arrows between them. We can compose arrows between objects, so if we have an arrow from A to B and an arrow from B to C, there is also an arrow from A to C (which can be constructed from the two arrows). In addition, there are two requirements about arrows:

Category theory is very abstract, because it does not tell us anything about the objects and arrows involved. They could be just anything. You can create a category by taking numbers and drawing arrow between a and b if a is greater than or equal to b, but you can also create a category from classes and draw arrows where a class can be used in place of another.

Functors and depth in hierarchy

There is one more idea from category theory that we need to understand covariance and contravariance and that is a functor. Let's say that we want to count the depth of the inheritance for objects in our class hierarchy. For example, object has depth zero; Animal and Random both have a single parent object. Finally, Fish and Cat both have depth two, since they have parent Animal, which has another parent object.

We can draw this as a transformation from our category of classes to a category of numbers - that is, a category with numbers 1, 2, 3, ... and an arrow between a and b if a >= b:

To be completely precise, we need to map objects as well as arrows of the category.

Note that the mapping has some interesting properties. All identity arrows on the left are mapped to the identity arrows on the right. Also, if we compose an arrow on the left (i.e. from Fish to Animal with an arrow from Animal to object) and then map it, we get the same arrow as if we first mapped the two arrows and then composed the result (arrow 2 >= 1 with an arrow 1 >= 0, which composes to an arrow 2 >= 0).

Definition of a functor

The mapping that we just described is exactly what category theorists call functor. To make this blog post fancier, let's look at the formal definition. Assuming that we have two categories C and D (i.e. our classes and numbers) a functor F from C to D is a mapping that:

In addition, two conditions must hold. Firstly, the arrow assigned to the identity arrow (arrow from A to A in category C) needs to be the identity arrow of the object F(A). Secondly, if we compose arrows and then map the composed arrow, the result should be the same as if we first map the arrows and then compose the result. The two conditions are exactly the same as the ones I mentioned at the end of the previous section. Although these are important in theory, we don't need to worry about them (because I will not cheat and when I say that something is a functor, it will actually obey these two laws!)

Functors and generic types

So far, I explained what a functor is and we looked at an example - mapping from a category of classes to a category of numbers that assigns the depth in a class hierarchy to every class. The example was interesting, because it mapped more complex structure (tree of classes) to a flat structure (just ordered numbers). In general, functors can do fairly complicated things. To explain covariance and contravariance, we need to look at pretty simple functors though.

Covariant functors

The definition of functor that I described earlier is actually a covariant functor. The covariant label means that it keeps the same orientation of arrows when mapping them. To an arrow A -> B, the functor assigns an arrow F(A) -> F(B). So, how does this relate to covariant generic types?

We can understand IEnumerable<T> as a functor that takes classes such as Cat or Animal to enumerable interfaces IEnumerable<Cat> and IEnumerable<Animal>, respectively:

The mapping between objects is quite simple. Given a type named Cat, the mapping gives us back a type IEnumerable<Cat> (and similarly for all other types). The mapping of arrows is also straightforward. If we have a type Cat that can be used in place of Animal (that is, we have an arrow Cat -> Animal), then the arrow is mapped to an arrow between IEnumerable<Cat> and IEnumerable<Animal>. This means that the mapping preserves orientation of arrows and so we can use IEnumerable<Cat> where an IEnumerable<Animal> is expected!

Okay, I bet you can already guess what is going to happen with the picture when we look at contravariant generic types such as IComparable...

Contravariant functors

The definition of functor that I showed earlier is actually a definition of covariant functor, but category theory also defines contravariant functors. The only difference is that contravariant functor reverses the orientation of all arrows. So, if we have a contravariant functor G from category C to D, it means that:

Let's look at an example. The IComparer<T> type is a contravariant functor. For objects, the mapping is quite simple. Given Animal, the functor gives us a type IComparer<Animal> (and similarly for other types). However, the mapping on arrows is interesting. If we have an arrow from Cat to Animal (saying that Cat can be used where Animal is expected), the mapped arrow is reversed, so we get an arrow from IComparer<Animal> to IComparer<Cat>. This corresponds to the C# example in the first section - we can use comparer on animals to compare cats. The following diagram shows how contravariant functor reverses arrows in our category of classes:

The diagram tells us some interesting things. By reversing the arrows, there is now an arrow from IComparer<object> to any other IComparer in the diagram. This means that we can use IComparer<object> to compare anything! (In the covariant case, any IEnumerable could have been used as an argument to method taking IEnumerable<object>.) Also, there is no terminal object so we cannot write a method that will take any IComparer as an argument. (This does not really have any practical implications, but it is interesting nevertheless!)

Summary

If you followed the article up to this point, then congratulations and thanks for staying with me! You learned quite a few concepts from general abstract nonsense branch of mathematics, but I hope you could see that they nicely correspond to features in C# 4.0. We started by looking at the definition of category and how it corresponds to class hierarchies (with classes as objects and inheritance as arrows). Then we looked at functors and how covariant functors model covariant generic types like IEnumerable<T> and contravariant functors model contravariant types like IComparer<T>.

Why category theory matters?

This may be an interesting intelectual exercise, but why should we care? Firstly, I think that reading this article gives you a better intuition about covariance and contravariance, which are quite hard to understand concepts.

If you're just going to use C# 4, then you don't really need to worry about category theory. If you do something wrong, the compiler will tell you (and will keep telling you until you get it right). Luckilly, the C# language designers understood the theory for us! However, even if you're not designing a language, knowing the theory might help you understand complex C# code. For example, take a look at the following example:

static void CallWithPrinter(Action<Action<Animal>> animalGenerator) {
  animalGenerator(animal => Console.WriteLine(animal.Name));
}

The method takes an Action delegate (which is contravariant) that takes another Action delegate as the input and does something. This nested delegate accepts any argument of type Animal. The method invokes the delegate and gives it a lambda function that takes an animal and prints its name. Now, the question is, can we use Action<Action<Cat>> as an argument to CallWithPrinter?

Action<Action<Cat>> catGenerator = handler => handler(new Cat("Otto"));
CallWithPrinter(catGenerator);

The sample creates a lambda function that takes a handler of type Action<Cat> and calls it with a sample cat as an argument. If you try to write this code in C# 4, it will compile and run fine. Why is that the case? Because we are applying Action<T> delegate two times and so we reverse the arrows twice! See the following diagram:

Understanding why the previous code snippet is correct just using the C# specification could be quite hard. The example is quite simple, so you might intuitively expect it to work (and hope that the compiler agrees), but intuition can be sometimes tricky. Understanding the theory gives you a powerful tool that you can use to better understand what is going on, even in complicated situations that combine covariant and contravariant types in crazy ways!

Published: Tuesday, 19 June 2012, 2:24 PM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: c#, research