What are covariance and countervariance?

Asked

Viewed 2,698 times

40

I saw something like in this question and I know that this relates in some way to object orientation.

What are they? How do they affect my code, and how can I use them to encode better?

3 answers

33


In object-oriented languages, if a function or variable expects to receive an object of a type you do not need to pass it an object of exactly this type: according to the the principle of Liskov’s replacement we can pass any other compatible object it also serves. For example, if a variable is of type Animal, we can assign her an object of the type Cachorro, since puppy objects implement all methods expected by the superclass interface.

Animal a = new Cachorro("rex");

So far so good, but the Liskov principle is a description of the behavior of things and to describe the system of types more rigorously we will need concrete rules, which is where the co-variance and the counter-variance will appear.

The first place where this problem appears are the types of methods. Suppose the Animal interface has a method foo:

interface Animal {
    Animal foo (Animal x);
}

The subclasses of foo also have to implement this method. But the types of the return parameters and value need to be exactly the same?

//invariante - ok
class Cachorro1 implements Animal {
    Animal foo (Animal x);
}

//parâmetro covariante - erro
class Cachorro2 implements Animal {
    Cachorro foo(Cachorro x){ ... }
}

//retorno contravariante - erro
class Cachorro3 implements Animal {
    Object foo(Object x){ ... }
}

//retorno covariante, parâmetro contravariante- ok
class Cachorro4 implements Animal {
    Cachorro foo (Object x);
}

Version 1 obviously fits, since the method type foo is exactly the same. But what about version 2 and 3? The class Cachorro2 does not obey the Liskov principle due to the type of parameter: we hope to be able to pass any Animal as a Foo parameter, but an object Cachorro2 does not accept Cats as a parameter, only others Cachorros. Similarly, class 3 breaks the substitution principle with the type of return: we hope that foo always return us a Animal, but an object Cachorro3 may return to us another Object whichever. Class 4, on the other hand, has no problem: it is less restricted in type than it accepts and has no problem being more specific in type of return.

In short, when is one method replaceable by another? If we have two types of function F = A -> B (function that takes A and returns B) and F' = A' -> B', then

 F' <: F
 se e somente se
 (A <: A') e (B' <: B)

Note that in the type of the parameter A and A' are in the order exchanged in relation to F and F' while in the case of the type of the return (B) they are in the same order. The type of return varies in the same direction as the method type (covariance) while the parameter type varies in the opposite direction (counter-variance)

The other place where the variance appears are in parameterized types, or Generics. And have a parameterized guy like List, and two types A <: B, what can we say about the types List<A> and List<B>? Who is subtype of whom? In this case the answer depends on the parameterized type and Maniero’s answer has some good examples:

List<A> is an invariant type relative to parameter A. It does not matter if A < B: nor List<A> will be subtype of List<B> nor vice versa.

Enumerable<A> is covariant with its parameter A: if A <: B then a Enumerable<A> <: Enumerable<B>

PrintAnimals<A> is contravariant with its parameter A: if A <: B then PrintAnimals<B> <: PrintAnimals<A>

By default, when you create a parameterized type the compiler will assume that its parameterized type is invariant with respect to the parameter types. If you want your type to be covariant or variant you need to add an annotation along with the parameter. In Scala (the question you linkou) this annotation is a + or - and in C# (like the Maniero examples) this annotation is in or out

31

Variance refers to how a type relates to its subtypes.

I’ll use examples in C# which is what I know.

First let’s go to an example of invariance:

<!lang:

IList<Animal> lista = new List<Dog>();

What happens if you try to add an element in lista like Cat? The compiler will refuse. And do well since this list should only accept Dogs. There is no guarantee that the inclusion of a Cat will not cause problems in lista which is being treated as a list of Animals.

But if we know that an operation is safe in certain relationships, we can indicate that the operation is covariant. A good example is an enumeration. You cannot change the type of an enumeration, so you can allow more freedom.

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

IEnumerable<Cat> cats = new List<Cat> { new Cat("Tom") };
PrintAnimals(rats);
IEnumerable<Mouse> mouses = new List<Mouse> { new Mouse("Jerry") };
PrintAnimals(mouses);

The code of PrintAnimals can only work because IEnumarable is covariant:

public interface IEnumerable <out T>: IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

This out is an indication of the type covariance. You are saying that the type T can be represented by a supertype (a more general type) without problems. This statement is indicating to the compiler that a IEnumerable of a more specific type, for example IEnumerable<Cat> can be treated as a more generic type, for example IEnumerable<Animal>.

Obviously this is a conscious choice of developer. He should only make this option if he is sure that the operation will not cause problems.

Without this indication of covariance the compiler will prevent the declaration of lista above.

To covariance indicates that a data collection can have its elements referenced by its supertypes (types it possesses by some form of inheritance).

To countervariance does the inverse indication. It allows a more specific type to be used in place of a more general type. Example for comparison operations:

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

In this case the in indicates countervariance and allows the following code to be valid:

void CompareCats(IComparer<Cat> comparer) {
  var cat1 = new Cat("Tom");
  var cat2 = new Cat("Frajola");
  if (comparer.Compare(cat1, cat2) > 0) 
    Console.WriteLine("Tom é maior!");
}

IComparator<Animal> compareAnimals = new AnimalSizeComparator(); //sabe como comparar Animals
CompareCats(compareAnimals);

I put in the Github for future reference.

This is saying that an object Animal can use a comparison of Cat without problems. The result obtained will be correct. That is, a more general type can benefit from a more specialized operation properly.

With the use of these techniques programs can be more flexible and can be compiled while maintaining type security. Without a developer-given certainty, the compiler will always prefer to consider types as invariants.

The subject is counterintuitive so everyone finds confusing.

7

What’s with the variance talk?

I will try to explain the way I have fixed this subject in my mind... in the most intuitive way for me, in the hope that the subject becomes as trivial and easy as possible.

In languages where types can be composed via a static parameter, the variance tells how the composite type varies according to the component type (it is easier to understand with examples! = D ).

I will explain using C# because it is the language that I master. I imagine that other languages have similar concepts. In C# only variance is applied to generic interfaces and delegates, that is, accepting types as parameters (statically).

It works that way:

interface IList<T> { ... }

Note that the interface IList receives a type per parameter called T. This is called a generic parameter in C#.

By using the interface we can make the composition, as in this example:

IList<int> listaDeInteiros;
  • Covariance: suppose an interface IS<T>. IS and T are covariant when IS varies along with T. So this is valid:

    Animal a = (Girafa)g;
    IS<Animal> ia = (IS<Girafa>)ig;
    
  • Counter-variance: suppose another interface IE<T>. IE and T are covariants is when IE varies contrary to T. So this is valid:

    Animal a = (Girafa)g;
    I<Girafa> ig = (I<Animal>)ia;
    

One question that remains is what makes a type covariant or counter-variant with respect to its parameter?

Or rather, what characteristic of the type makes the above examples true?

I will explain using the interfaces of the above examples.

What characterizes the covariance

The use of T only as an output of the type IS<T>. Therefore, in C#, the generic parameter with out:

interface IS<out T> // T só poderá ser usado como saída
{
    T LerValor(); // método que tem saída do tipo T
}

Let’s test it to see if it’s gonna be a problem:

IS<Animal> ia = (IS<Girafa>)ig;
Animal a = ia.LerValor(); // parece bom... IS<Girafa>.LerValor()
                          // retorna Girafa, que é um Animal.
                          // Beleza!

In human language, something that returns only giraffes, can be treated as something that returns animals.

What characterizes counter-variance

The use of T only as input of type IE<T>. Therefore, in C#, the generic parameter with in:

interface IE<in T> // T só poderá ser usado como entrada
{
    void EscreveValor(T valor); // método cuja entrada é do tipo T
}

Let’s test it to see if it’s gonna be a problem:

IE<Girafa> ig = (IE<Animal>)ia;
ig.EscreveValor( (Girafa)g ); // parece bom... IE<Animal>.EscreveValor(x)
                              // recebe Animal, então se eu só puder passar
                              // Girafa tá de boa, pois Girafa é Animal.
                              // Beleza!

In human language, something that receives animals can be treated as something that receives only giraffes.

Composition of various levels of variance

It’s easier to understand using delegates in this case.

Delegates are definitions of functions... is a function signature, so to speak.

I’ll define them like this:

delegate T DS<out T>(); // tipo de função que retorna um valor T
delegate void DE<in T>(T valor); // tipo de função que recebe o valor T

Let’s go to some affirmations and some codes to demonstrate:

  • The exit is an exit

        DS<DS<Girafa>> ssg = () => () => new Girafa();
        DS<DS<Animal>> ssa = ssg;
    
        // vou receber uma girafa (como sendo um animal)
        Animal a = ssa()();
    
  • The exit entrance is an entrance

        DS<DE<Animal>> sea = () => a => Console.WriteLine(a);
        DS<DE<Girafa>> seg = sea;
    
        // vou passar uma girafa (mas o delegate sabe usar qualquer animal)
        var g = new Girafa();
        seg()(g);
    
  • The exit from the entrance is an entry

        DE<DS<Animal>> esa = sa => Console.WriteLine(sa());
        DE<DS<Girafa>> esg = esa;
    
        // vou passar uma girafa (mas o delegate sabe usar qualquer animal)
        var g = new Girafa();
        esg(() => g);
    
  • The entrance to the entrance is an exit

        DE<DE<Girafa>> eeg = eg => eg(new Girafa());
        DE<DE<Animal>> eea = eeg;
    
        // vou receber uma girafa (através do delegate)
        Animal a;
        eea(a2 => a = a2);
    

I tried to make an image to explain, I don’t know if you’re confused, if you’re telling me I change or take it.

Composição de níveis de variância

Titanic compositions

Let’s go to some more, shall we say, complex examples:

  • The entrance of the exit from the entrance to the entrance... what would it be? I already answer: it is entrance.

        DE<DE<DS<DE<Animal>>>> eeseg = null;
        DE<DE<DS<DE<Girafa>>>> eesea = eeseg;
    
  • And the entrance of exit 5 from the exit of the entrance to the entrance... what would it be? Straight to the point: is exit.

        DE<DE<DS<DE<DS<DS<DS<DS<DS<DE<Girafa>>>>>>>>>> eesessssseg = null;
        DE<DE<DS<DE<DS<DS<DS<DS<DS<DE<Animal>>>>>>>>>> eesesssssea = eesessssseg;
    

You can answer these questions very quickly. Just count the entries.

  • Even number of entries is output

  • Odd number of entries is input

But what about the exits?

A: Exits do not affect anything

  • Output 10000: is output because it has even number of inputs (0 is even)

  • Output 100 from output 101: is input because it has odd number of inputs (only 1 input)

Browser other questions tagged

You are not signed in. Login or sign up in order to post.