I saw something like and I know 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?
In object-oriented languages, if a function or variable expects to receive an object of a type you do not need to pass it exactly one object: Liskov replacement principle we can pass any other compatible object that it also serves. For example, if a variable is of type Animal
, we can assign it an object of type Cachorro
, since dog 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 type system more strictly we will need concrete rules, which is where will appear the co- and the counter-variance .
The first place where this problem appears is the types of methods. Suppose the Animal interface has a foo
method:
interface Animal {
Animal foo (Animal x);
}
Subclasses of foo
also have to implement this method. But the return and parameter types 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 works, since the type of method foo
is exactly the same. But what about version 2 and 3? The Cachorro2
class does not obey the Liskov principle because of the parameter type: we expect to be able to pass any Animal
as a Foo parameter, but a Cachorro2
object does not accept Cats as a parameter, only other Cachorros
. Similarly, class 3 breaks the substitution principle with the return type: we expect foo
to always return a Animal
, but a Cachorro3
object can return us another Object
any. Class 4 has no problem: it is less constrained in the type than it accepts and it is okay to be more specific in the return type.
In short, when is one method replaceable by another? If we have two function types F = A - > B (function that receives A and returns B) and F '= A' -> B ', then
F' <: F
se e somente se
(A <: A') e (B' <: B)
Note that in parameter type A and A 'are in the order exchanged in relation to F and F' whereas in the case of the return type (B) they are in the same order. The return type varies in the same direction as the covariance type while the parameter type varies in the opposite direction (counter-variance)
The other place where the variance appears is in the parameterized types, or generics . And we have a type parameterized as List
, and two types A & lt ;: B, what can we say about types List<A>
and List<B>
? Who is the 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 with respect to parameter A. It does not matter if A < B: neither List<A>
will be subtype of List<B>
nor vice versa.
Enumerable<A>
is covariant with its parameter A: if A
Variance refers to how a type relates to its subtypes.
I'm going to use C # examples which is what I know.
First let's look at an invariance example:
IList<Animal> lista = new List<Dog>();
What happens if you try to add an element to lista
that is of type Cat
? The compiler will refuse. And it does well since this list should only accept Dog
s. There is no way to ensure that adding a Cat
will not cause problems with lista
being treated as a list of Animal
s.
But if we know that an operation is safe in certain relations, we can indicate that the operation is covariant. A good example is an enumeration. You can not 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 the indication of type covariance. You are saying that the type T
can be represented by a supertype (a more general type) without problems. This declaration 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 developer's conscious choice. It should only do this if you are sure that the operation will not cause problems.
Without this covariance statement the compiler will prevent the declaration of lista
above.
A contravariance makes the reverse statement. It allows a more specific type to be used instead 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 the contravariance 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've placed it on GitHub for future reference .
This is saying that a Animal
object can use a Cat
comparison with no problems. The result 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 certainty given by the developer, the compiler will always prefer to consider the types as invariant.
The subject is counter intuitive so everyone finds it confusing.
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 will become as trivial and easy as possible.
In languages where types can be composed via a static parameter, the variance says 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 other languages have similar concepts. In C # only variance is applied to generic interfaces and delegates, that is, they accept types as parameters (statically).
It works like this:
interface IList<T> { ... }
Note that the IList
interface receives one type per parameter called T
. This is called a generic parameter in C #.
When 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;
Anti-variance: suppose another interface IE<T>
. IE
and T
are covariates is IE
varies contrary to T
. So this is valid:
Animal a = (Girafa)g;
I<Girafa> ig = (I<Animal>)ia;
A leftover question is what makes a type covariant or counter-variant with respect to its parameter?
Or rather, what type characteristic makes the above examples true?
I'll explain using the interfaces of the examples above.
The use of T
only as output of type IS<T>
.
So in C #, the generic parameter with out
is written:
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 to see if it's going to 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.
The use of T
only as input of type IE<T>
.
So in C #, the generic parameter with in
is written:
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 to see if it's going to 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.
It's easier to understand using delegates in this case.
Delegates are role definitions ... it's 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
Here are some statements and some code to demonstrate:
Output output is output
DS<DS<Girafa>> ssg = () => () => new Girafa();
DS<DS<Animal>> ssa = ssg;
// vou receber uma girafa (como sendo um animal)
Animal a = ssa()();
The input of the output is an input
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 output of the input is an input
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);
Input input is output
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 do not know if it is confusing, if you are told that I change or shoot it.
Titanic compositions
Let's take a few more, let's say, complex examples:
The input from the input input output ... what would it be? I already answer: it's input.
DE<DE<DS<DE<Animal>>>> eeseg = null;
DE<DE<DS<DE<Girafa>>>> eesea = eeseg;
And the input of output ^ 5 from input input of output input ... what would it be? Straight to the point: it's output.
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;
These questions can be answered very quickly. Just count the entries.
Number of inputs is output
Odd number of entries is input
But what about the exits?
R: Exits do not affect at all
The output of the ^ 10000 output is output because it has even number of inputs (0 is even)
The output ^ 100 of the output input ^ 101: is input because it has odd number of inputs (only 1 input)