Question:
To demonstrate the problem I will use Scala code (although this is a rule formalized by Luca Cardelli ).
trait Function1[-A, +R] {
def apply(x: A): R
}
In Scala this means that a function with one element is contravariant on the input and covariant on the output.
That is, we can only consider a function f: (X => Y)
to be a subtype of a function g: (X' => Y')
if X
is a supertype of X'
and Y
is a subtype of Y'
.
Why does this occur?
Answer:
The justification for the input type being contravariant [and the output being covariant] is to satisfy the Liskov Substitution Principle . This principle states that "subclasses should always be less constrained than their superclasses". In other words, where an object of the base class could be used, an object of the subclass should also be able to be used.
So, if we have a legacy code that calls a method foo
of the base class, passing Bar
as a parameter and receiving Baz
, and this method is overridden (and not just overloaded) in the subclasses, it is necessary that they:
-
Accept at least
Bar
as an argument; they can accept more thanBar
, but not less. If a subclass decides to accept, for example, anything (ieObject
), no problem: it will still be acceptingBar
.Since any superclass of
Bar
meets this requirement, the type of the input parameter is contravariant. -
Return a
Baz
– compatible object; they can return something more specific thanBaz
, but not something incompatible with it (ie that has a stricter interface, missing fields/methods, etc). As subclass objects can be used in place of base class objects (by Liskov's own principle), they can be used as a return value (ie the output type is covariant).
This decision [to use output covariance and input contravariance] ensures type safety , reducing/eliminating runtime type errors. More restricted strategies (eg, invariance) also have the same effect, but without the convenience of customizing the behavior of subclasses as needed.
It's also worth mentioning that there are languages (like Eiffel ) that support covariant input types . This strategy can be convenient in many situations, although it is not 100% foolproof. Example (using Java syntax – more familiar than Eiffel's):
class AbrigoAnimais {
void adicionarAnimal(Animal a) { ... }
Animal obterAnimal() { ... }
}
class AbrigoGatos extends AbrigoAnimais {
void adicionarAnimal(Gato g) { ... } // Entrada covariante (não typesafe)
Gato obterAnimal() { ... } // Saída covariante (typesafe)
}
AbrigoGatos abrigo = new AbrigoGatos();
// Erro em tempo de compilação
abrigo.adicionarAnimal(new Cachorro());
// Erro em tempo de execução
((AbrigoAnimais)abrigo).adicionarAnimal(new Cachorro());