#What Is Covariance and Contravariance in Programming? – CloudSavvy IT

Table of Contents
“#What Is Covariance and Contravariance in Programming? – CloudSavvy IT”

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.
If you liked the article, do not forget to share it with your friends. Follow us on Google News too, click on the star and choose us from your favorites.
For forums sites go to Forum.BuradaBiliyorum.Com
If you want to read more like this article, you can visit our Technology category.