Co- and Contravariance

With a simple example, the concept of co- and contravariance is understood. Forever and for everybody! With examples for scala and java developers.

Inheritance #

Let’s start slowly. Concepts in our problem domain can be expressed using inheritance. We usually express this graphically as follows:

+-----------+        +----------+
|           |        |          |
|    A      |        |  Animal  |
|           |        |          |
+-----------+        +----------+
     ^                    ^ 
     |                    | 
+----+------+        +----+-----+
|           |        |          |
|    B      |        |  Cat     |
|           |        |          |
+-----------+        +----------+

Thus we expressed that B is a special kind of A, or that a Cat is a special kind of Animal. Usually, when working with inheritance we want to adhere to the inheritance contract: that is, we want to be able to use the specialised object anywhere where a more general object is required.

For our own concern - covariance, invariance or contravariance - we just observe that these concepts do not matter yet.

Invariance, Covariance and Contravariance #

In the next step we will put the objects of our classes into some other structure. This structure could be a list, a vector, an option, a future. Lets just call it F for the time being. E.g. we talk about a F of Animals, a F of Cats, a F of As, a F of Bs.

In scala, in the simplest possible way, we could start to define a structure F, which operates on As, Bs, Animals or Cats like

// this does nothing - it just allows us to create typed F instances
case class F[A]()

And now - with the structure F in place - we can start to talk about the variance of its type parameter A. Syntactically we can differentiate three cases (there are three cases of specifying the type paramter A in our structure F):

F[A]
A is invariant
F[+A]
A is covariant
F[-A]
A is contravariant

Now let us assume we have defined Animal and Cat as in the example below:

trait Animal {}
trait Cat extends Animal {}

then we can, respectively cannot, do the following:

// Cat is an animal
scala> val animal: Animal = new Cat {}
val animal: Animal = $anon$1@100bba26

// An animal is not a cat
scala> val cat: Cat = new Animal {}
                      ^
       error: type mismatch;
        found   : Animal
        required: Cat

The example above still has nothing do to with variance. We need to use our F.

Invariance #

In the example above (no + or - in the type parameter) F is invariant in A. Invariance implies that even if the type parameter is in an inheritance relationship, the structure F is not.

+----------+   +----------+   +----------+
|          |   |          |   |          |
|  Animal  |   | F[Animal]|   | F[Cat]   |
|          |   |          |   |          |
+----------+   +----------+   +----------+
     ^                                   
     |                                    
+----+-----+                              
|          |                              
|  Cat     |                              
|          |                              
+----------+                              

So, if Cat is an Animal, then F[Cat] is not an F[Animal] AND F[Animal] is not a F[Cat]. That is the case because we did decide that F should behave this way, since we made the type paramter A invariant in F.


// Pretend that F is covariant.

scala> val f_animal : F[Animal] = F[Cat] ()
                                         ^
       error: type mismatch;
        found   : F[Cat]
        required: F[Animal]
       Note: Cat <: Animal, but class F is invariant in type A.
       You may wish to define A as +A instead. (SLS 4.5)

// Pretend that F is contravariant

scala> val f_cat : F[Cat] = F[Animal]()
                                     ^
       error: type mismatch;
        found   : F[Animal]
        required: F[Cat]
       Note: Animal >: Cat, but class F is invariant in type A.
       You may wish to define A as -A instead. (SLS 4.5)

Compare this with the error message, when the type is not in an inheritance relationship with Cat at all:

scala> val f_cat : F[Cat] = F[Person]()
                                     ^
       error: type mismatch;
        found   : F[Person]
        required: F[Cat]

Also have a look at the error messages above: You may wish to define A as +A instead. (SLS 4.5) and You may wish to define A as -A instead. (SLS 4.5). This is the compiler telling us that we can consider to make A covariant (+) or contravariant (-).

Covariance (+) #

If we define

case class F[+A]()

then F is covariant in A. We say that if Cat is an Animal, then F[Cat] is an F[Animal] as well.

+----------+   +----------+
|          |   |          |
|  Animal  |   | F[Animal]|
|          |   |          |
+----------+   +----------+
     ^              ^  
     |              |      
+----+-----+   +----+-----+
|          |   |          |
|  Cat     |   | F[Cat]   |
|          |   |          |
+----------+   +----------+

With this definition of F, the code which previously had errors in all three cases now works in the case of:

scala> val f_animal : F[Animal] = F[Cat]()
val f_animal: F[Animal] = F()

Contravariance (-) #

If we define

case class F[-A]()

then F is contravariant in A. We say that if Cat is an Animal, then F[Animal] is an F[Cat] as well.

+----------+   +----------+   +----------+
|          |   |          |   |          |
|  Animal  |   | F[Cat]   |   | F[Animal]|
|          |   |          |   |          |
+----------+   +----------+   +----+-----+
     ^              ^      OR      |      
     |              |              v      
+----+-----+   +----+-----+   +----------+
|          |   |          |   |          |
|  Cat     |   | F[Animal]|   | F[Cat]   |
|          |   |          |   |          |
+----------+   +----------+   +----------+

Please note how the inheritance arrow of F is strangely inverted. With this definition of F, the code which previously had errors with invariance in all three cases now works in the case of:

scala> val f_cat : F[Cat] = F[Animal]()
val f_cat: F[Cat] = F()

Does it have any use, whatsoever? #

Contravariance #

Yes, it does. To illustrate contravariance with a practical example, let’s consider animal feeders.

trait Animal {
  def eat(): Unit = println("Animal eating")
}

trait Cat extends Animal {
  override def eat(): Unit = println("Cat eating fish")
}

// A contravariant feeder class
case class Feeder[-A](feed: A => Unit)

// Create our feeders
val animalFeeder = Feeder[Animal](animal => println("Feeding standard animal food"))
val catFeeder = Feeder[Cat](cat => println("Feeding premium cat food"))

// This works! Someone expecting a cat feeder can use an animal feeder
// since it knows how to feed any animal (including cats)
val myCatFeeder: Feeder[Cat] = animalFeeder  // ✅ Type-safe assignment

// This would NOT work - someone expecting to feed any animal
// cannot use a specialized cat feeder
// val myAnimalFeeder: Feeder[Animal] = catFeeder  // ❌ Type error!
+----------+       +--------------+
|          |       |              |
|  Animal  |       | Feeder[Cat]  |
|          |       |              |
+----------+       +--------------+
     ^                   ^        
     |                   |        
     |                   |        
+----+-----+       +-----+---------+
|          |       |              |
|  Cat     |       |Feeder[Animal]|
|          |       |              |
+----------+       +--------------+

Notice how the inheritance arrow is inverted! While Cat is a subtype of Animal, Feeder[Animal] is a subtype of Feeder[Cat]. This is the essence of contravariance.

The intuition: If you have a feeder that can feed ANY animal, it can definitely feed your cat. But if you have a feeder that only knows how to feed cats, it cannot necessarily feed any arbitrary animal.

This same pattern applies to function inputs, which is why in Scala a function is defined as Function[-Input, +Output]. The function is contravariant in its input type.

Covariance #

While contravariance is about consuming values (inputs), covariance is about producing values (outputs). Let’s create a complementary example using animal shelters:

// Same hierarchy as before
trait Animal {
  def speak(): String = "Some animal sound"
}

trait Cat extends Animal {
  override def speak(): String = "Meow"
}

// A covariant shelter that provides animals
case class Shelter[+A](get: () => A)

// Create our shelters
val catShelter = Shelter[Cat](() => new Cat {})
val animalShelter = Shelter[Animal](() => new Animal {})

// This works! Someone expecting any animal can accept a cat
val myAnimalShelter: Shelter[Animal] = catShelter  // ✅ Type-safe assignment

// This would NOT work - someone expecting a cat
// cannot accept just any animal
// val myCatShelter: Shelter[Cat] = animalShelter  // ❌ Type error!
+----------+       +---------------+
|          |       |               |
|  Animal  |       |Shelter[Animal]|
|          |       |               |
+----------+       +---------------+
     ^                   ^        
     |                   |        
     |                   |        
+----+-----+       +-----+--------+
|          |       |              |
|  Cat     |       | Shelter[Cat] |
|          |       |              |
+----------+       +--------------+

Notice that unlike with contravariance, the inheritance arrows point in the same direction! If Cat is a subtype of Animal, then Shelter[Cat] is a subtype of Shelter[Animal].

The intuition: If you need a shelter that provides animals, a cat shelter works fine because cats are animals. But if you specifically need a cat shelter, a general animal shelter won’t suffice because it might give you a dog or some other non-cat animal.

This same pattern applies to function outputs, which is why in Scala a function is defined as Function[-Input, +Output]. The function is covariant in its output type.

We can summarize the pattern:

  1. Contravariance (-A): Used when a type consumes values of type A

    • If B extends A, then Consumer[A] extends Consumer[B]
    • Example: Feeder[-A], Function[-A, B]
  2. Covariance (+A): Used when a type produces values of type A

    • If B extends A, then Producer[B] extends Producer[A]
    • Examples: Shelter[+A], Function[A, +B], List[+A]

For Java Developers #

If you’re coming from Java, you might find Scala’s variance annotations a bit different. In Java, variance is declared at the usage site rather than at the declaration site (as in Scala).

Here’s how our examples would look in Java:

Java Syntax for Variance #

// Our type hierarchy
interface Animal {
    void eat();
}

interface Cat extends Animal {
    @Override
    void eat();
}

// Invariance - standard generic with no wildcards
class Container<T> { /* ... */ }

// Covariance - using extends wildcard (similar to +T in Scala)
// "? extends Animal" means "Animal or any subtype of Animal"
Container<? extends Animal> covariantContainer;

// Contravariance - using super wildcard (similar to -T in Scala)
// "? super Cat" means "Cat or any supertype of Cat"
Container<? super Cat> contravariantContainer;

Translating Our Examples to Java #

For contravariance (Feeder example):

interface AnimalFeeder<T> {
    void feed(T animal);
}

// In Java, we'd use the super wildcard for contravariance:
AnimalFeeder<Animal> generalFeeder = animal -> System.out.println("Feeding generic food");
AnimalFeeder<Cat> catFeeder = cat -> System.out.println("Feeding cat food");

// This works - using an animal feeder where a cat feeder is expected
AnimalFeeder<? super Cat> feederForMyCat = generalFeeder;  // ✅ Legal

// This doesn't work - cat feeder can't handle all animals
// AnimalFeeder<? super Animal> feederForAllAnimals = catFeeder;  // ❌ Illegal

For covariance (Shelter example):

interface Shelter<T> {
    T getAnimal();
}

// In Java, we'd use the extends wildcard for covariance:
Shelter<Animal> animalShelter = () -> new Animal() { /* ... */ };
Shelter<Cat> catShelter = () -> new Cat() { /* ... */ };

// This works - using a cat shelter where an animal shelter is expected
Shelter<? extends Animal> myAnimalShelter = catShelter;  // ✅ Legal

// This doesn't work - animal shelter might not give cats
// Shelter<? extends Cat> myCatShelter = animalShelter;  // ❌ Illegal

The key difference is that Scala lets you declare variance at the type definition (class Shelter[+A]), while Java requires wildcards at each usage site (Shelter<? extends Animal>).

Java’s approach is more flexible but more verbose, while Scala’s approach is more concise but requires carefully designing types with variance in mind from the start.

Contravariance in the Wild #

Contravariance isn’t just a theoretical concept - it’s widely used in real-world libraries and frameworks. Let’s look at some examples from well-known Java APIs to see how contravariance helps create more flexible and powerful interfaces.

Example 1: Java Collections Framework #

The Java Collections Framework makes extensive use of contravariance. One of the clearest examples is the Collections.copy() method:

public static <T> void copy(List<? super T> dest, List<? extends T> src)

This method copies elements from a source list to a destination list. Notice the contravariant parameter List<? super T> for the destination list. This allows you to copy elements into a list of any supertype of T, which is extremely useful in practice.

For example, if you have:

List<Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
cats.add(new Cat("Fluffy"));

// This works! You can copy Cats into an Animal list
Collections.copy(animals, cats);

Without contravariance, you would need exact type matching, which would severely limit the flexibility of the API.

Example 2: Java Functional Interfaces #

The Iterable interface’s forEach method demonstrates another practical use of contravariance:

default void forEach(Consumer<? super T> action)

This allows you to pass a Consumer that can handle a supertype of the elements in the collection. For example:

// A consumer that works with any Animal
Consumer<Animal> animalFeeder = animal -> animal.feed();

// A list of specific Cat objects
List<Cat> cats = getCats();

// Using contravariance, we can feed all cats with our animal feeder
cats.forEach(animalFeeder);

Without contravariance, we would need to create a new Consumer<Cat> even though our existing Consumer<Animal> could already handle cats perfectly well.

Example 3: Comparators #

Java’s sorting methods often use contravariant comparators:

<T> void sort(List<T> list, Comparator<? super T> c)

This means you can use a comparator for a supertype to compare more specific types. For instance, you could sort a list of ElectricCar objects using a Comparator<Vehicle> that compares vehicles by their manufacturing year.

That’s it - viel Spaß and happy coding!

Kommentare