Covariance & Contravariance

Mátyás Lancelot Bors
5 min readMar 4, 2018

--

Image from Pexels

Through this article, we are going to take a look at some terms like “covariance” and “contravariance” to understand to which concepts they are related to.

Introduction

Sometimes in the happy and sparkling world of programming, we can meet some terms that we have hardly heard before. More often, those terms are borrowed from other domains and seem to hide very advanced concepts while, indeed, they encapsulate very simple ideas.

A small definition

Covariance and contravariance come from mathematics. It is a measure of how changes in one variable are associated with changes in a second variable. It measures the degree to which two variables are linearly associated. In other words, it describes what happens when a change occurs. If the result goes in the same direction as the change, it is covariant, otherwise it is contravariant.

We are not going to dig deeper with mathematical notions. We are rather going to take a look at how these concepts are translated in the world of computer science. First, we have to understand Subtyping.

Subtyping

Subtyping is a notion where a subtype, which is a data type, is related to a supertype. Subtypes are an essential concept in object-oriented programming and are substitutable to supertypes. There is a difference between Inheritance and Subtyping and it can be defined like so:

  • Subtyping refers to compatibility of interfaces. A type B is a subtype of A if every function that can be invoked on an object of type A can also be invoked on an object of type B.
  • Inheritance refers to reuse of implementations. A type B inherits from another type A if some functions for B are written in terms of functions of A.

In other words, Subtyping refers to the compatibility of interfaces. Inheritance refers to reuse of implementations. Subtyping is about implementing an interface, and so being able to substitute different implementations of that interface at runtime. Inheritance is about gaining attributes and functionalities of super types.

Subtyping relation is defined by the type system of a programming language. A type system is a set of rules that assigns a type, which is a property, to the various constructs of a program (variables, expressions, functions, modules). The way to assign types is defined by type rules. A programming language has primitive data types. Those basic types are considered to be built using nullary type constructors, or, in other words, constructors without any argument. A type constructor is a feature that builds new types from old ones. A type constructor is an n-ary (number of arguments) type operator taking as argument zero or more types, and returning another type.

Object hierarchies and substitution

Covariance and contravariance have a slightly different meaning in programming than in mathematics. The idea is related to the hierarchy of types.

Let’s say we have two classes, A and B. Let’s say that B is derived from type A. So, in other words, A is the base class for B. So B contains every method and property that type A does and more. So, we can say that B is “bigger” than A. So, here we have a way of ordering classes. We can also say that A is higher in the type hierarchy than B.

If B derives from A, it means that it contains everything that A contains. So, we can use B anywhere that we could have used A. Of course, we can’t always use A in place of B. The relationship is asymmetrical. This is formally known as the Liskov Substitution principle. This principle also states that everything that is true for A should be true for B.

Formal definitions

Knowing what we know, let’s try to elaborate some formal definitions.

A typing rule or a type constructor is:

  • covariant if it preserves the ordering of types, which orders types from more specific to more generic
  • contravariant if it reverses this ordering
  • bivariant if it is covariant and contravariant at the same time
  • invariant or nonvariant if it is neither covariant or contravariant

So, we can say that Covariance and contravariance are about very specific situations, where we can treat one type as if it were another type in a certain context. Covariance allows a “bigger”, or less specific, type to be substituted in an API where the original type is only used in an “output” position. contravariance allows a “smaller”, or more specific, type to be substituted in an API where the original type is only used in an “input” position. Covariance and contravariance are the ability of a programming language to take advantage of commonalities between generic types deduced from known commonalities of their type arguments.

Examples

Let’s try to use examples to illustrate a little more what we have seen until now. Let’s imagine that we have the following class:

public class Person
{
public string Name { get; set; }
}

Now, we have a derived class:

public class Client : Person { }

Let’s now imagine the following classes:

public class MailingList
{
public void Add(IEnumerable<out Person> persons) { }
}
public class Company
{
public IEnumerable<Client> GetClients() { }
}

We can now make the following operations:

var clients = company.GetClients();
var mailingList = new MailingList();
mailingList.Add(clients);

Here, we use a more derived type, Client, with the Add() method. So, this last method is covariant. Covariance allows us to pass a derived type where a base type is expected.

Let’s now try to illustrate contravariance. Let’s imagine the following base class and derived class:

class A { }
class B : A { }

Suppose that we have a delegate:

public delegate A MyDelegate(B b);

Now can imagine the following code:

class Program
{
static A Method1(A a)
{
Console.WriteLine("Method 1");
return new A();
}
static B Method2(B b)
{
Console.WriteLine("Method 2");
return new B();
}
static void Main(string[] args)
{
MyDelegate myDelegate = Method1;
myDelegate(new B());
}

}

contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.

Conclusion

Through this article we took a look at the concepts of Covariance and contravariance. We saw that those terms were borrowed from mathematics and are relative to Subtyping, more specifically to object hierarchies and substitution. We saw that Covariance allows us to pass a derived type where a base type is expected. On the other hand, contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.

One last word

If you like this article, you can consider supporting and helping me on Patreon! It would be awesome! Otherwise, you can find my other posts on Medium and Tumblr. You will also know more about myself on my personal website. Until next time, happy headache!

--

--