funções – First-class functions: Why must input types be contravariant?

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:

  1. Accept at least Bar as an argument; they can accept more than Bar , but not less. If a subclass decides to accept, for example, anything (ie Object ), no problem: it will still be accepting Bar .

    Since any superclass of Bar meets this requirement, the type of the input parameter is contravariant.

  2. Return a Bazcompatible object; they can return something more specific than Baz , 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());
Scroll to Top