X
Popular Searches

What Is Covariance and Contravariance in Programming?

Abstract image showing expanding yellow rectangles
Pixabay / geralt

Covariance and contravariance are terms which describe how a programming language handles subtypes. The variance of a type determines whether its subtypes can be used interchangeably with it.

Variance is a concept that can seem opaque until a concrete example is provided. Let’s consider a base type Animal with a subtype of Dog.

interface Animal {
    public function walk() : void;
}
 
interface Dog extends Animal {
    public function bark() : void;
}

All “Animals” can walk, but only “Dogs” can bark. Now, let’s consider what happens when this object hierarchy is used in our application.

Wiring Interfaces Together

Since every Animal can walk, we can create a generic interface that exercises any Animal.

interface AnimalController {
 
    public function exercise(Animal $Animal) : void;
 
}

The AnimalController has an exercise() method that typehints the Animal interface.

interface DogRepository {
 
    public function getById(int $id) : Dog;
 
}

Now we have a DogRepository with a method that is guaranteed to return a Dog.

What happens if we try to use this value with the AnimalController?

$AnimalController -> exercise($DogRepository -> getById(1));

This is permissible in languages where covariant parameters are supported. AnimalController has to receive an Animal. What we’re passing is actually a Dog, but it still satisfies the Animal contract.

This kind of relationship is particularly important when you’re extending classes. We might want a generic AnimalRepository that retrieves any animal without its species details.

interface AnimalRepository {
 
    public function getById(int $id) : Animal;
 
}
 
interface DogRepository extends AnimalRepository {
 
    public function getById(int $id) : Dog;
 
}

DogRepository modifies the contract of AnimalRepository—as callers will get a Dog instead of an Animal—but doesn’t fundamentally change it. It’s just being more specific about its return type. A Dog is still an Animal. The types are covariant, so DogRepository‘s definition is acceptable.

Looking at Contravariance

Let’s now consider the inverse example. It might be desirable to have a DogController, which alters the way in which “Dogs” are exercised. Logically, this could still extend the AnimalController interface. However, in practice, most languages won’t allow you to override exercise() in the necessary way.

interface AnimalController {
 
    public function exercise(Animal $Animal) : void;
 
}
 
interface DogController extends AnimalController {
 
    public function exercise(Dog $Dog) : void;
 
}

In this example, DogController has specified that exercise() only accepts a Dog. This conflicts with the upstream definition in AnimalController, which permits any “Animal” to be passed. To satisfy the contract, DogController must, therefore, also accept any Animal.

At first glance, this can seem confusing and unhelpful. The reasoning behind this restriction becomes more clear when you’re typehinting against AnimalController:

function exerciseAnimal(
    AnimalController $AnimalController,
    AnimalRepository $AnimalRepository,
    int $id) : void {
 
    $AnimalController -> exercise($AnimalRepository -> getById($id));
}

The problem is that AnimalController could be an AnimalController or a DogController—our method isn’t to know which interface implementation it’s using. This is down to the same rules of covariance which were useful earlier.

As AnimalController might be a DogController, there’s now a serious runtime bug awaiting discovery. AnimalRepository always returns an Animal, so if $AnimalController is a DogController, the application is going to crash. The Animal type is too vague to pass to the DogController exercise() method.

It’s worth noting that languages that support method overloading would accept DogController. Overloading permits you to define multiple methods with the same name, provided that they have different signatures (They have different parameter and/or return types.). DogController would have an extra exercise() method that only accepted “Dogs.” However, it would also need to implement the upstream signature accepting any “Animal.”

Handling Variance Issues

All of the above can be summarised by saying that function return types are allowed to be covariant, while argument types should be contravariant. This means that a function may return a more specific type than the interface defines. It may also accept a more abstract type as an argument (although most popular programming languages don’t implement this).

You most often encounter variance issues while working with generics and collections. In these scenarios, you often want an AnimalCollection and a DogCollection. Should DogCollection extend AnimalCollection?

Here’s what these interfaces could look like:

interface AnimalCollection {
    public function add(Animal $a) : void;
    public function getById(int $id) : Animal;
}
 
interface DogCollection extends AnimalCollection {
    public function add(Dog $d) : void;
    public function getById(int $id) : Dog;
}

Looking first at getById(), Dog is a subtype of Animal. The types are covariant, and covariant return types are allowed. This is acceptable. We observe the variance issue again with add() though—DogCollection must allow any Animal to be added in order to satisfy the AnimalCollection contract.

This issue is usually best addressed by making the collections immutable. Only allow new items to be added in the collection’s constructor. You can then eliminate the add() method altogether, making AnimalCollection a valid candidate for DogCollection to inherit from.

Other Forms of Variance

Besides covariance and contravariance, you may also come across the following terms:

  • Bivariant: A type system is bivariant if both covariance and contravariance simultaneously apply to a type relationship. Bivariance was used by TypeScript for its parameters prior to TypeScript 2.6
  • Variant: Types are variant if either covariance or contravariance applies.
  • Invariant: Any types that are not variant.

You’ll usually be working with covariant or contravariant types. In terms of class inheritance, a type B is covariant with a type A if it extends A. A type B is contravariant with a type A if it’s the ancestor to B.

Conclusion

Variance is a concept that explains the limitations within type systems. Usually, you only need to remember that covariance is accepted in return types, whereas contravariance is used for parameters.

The rules of variance arise from the Liskov substitution principle. This states that you should be able to replace instances of a class with instances of its subclasses without altering any of the properties of the wider system. That means that if Type B extends Type A, instances of A may be substituted with instances of B.

Using our example above means that we must be able to substitute Animal with Dog, or AnimalController with DogController. Here, we see again why DogController cannot override exercise() to only accept Dogs—we’d no longer be able to substitute AnimalController with DogController, as consumers currently passing an Animal would now need to provide a Dog instead. Covariance and contravariance enforce the LSP and ensure consistent standards of behavior.

James Walker James Walker
James Walker is a contributor to CloudSavvy IT. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience of managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker and Kubernetes. Read Full Bio »

The above article may contain affiliate links, which help support CloudSavvy IT.