Covariance and Contravariance of Producers and Consumers

In his book Effective Java, Joshua Bloch coined the pneumonic device PECS: Producer Extends, Consumer Super. This device can help us remember which Java keyword (extends or super) to use when specifying the bounds on generic types.

Unfortunately, we don't have a similar pneumonic for remembering the + notation for covariance or the - notation for contravariance in Scala. You need to remember that producers are +covariant, and consumers are -contravariant. The following example shows why producers are covariant and consumers are contravariant in terms of substitutability

Example

Let's say that we have some generic boxes that can hold whatever we want.

case class Box[+T](value: T)

Fuels

We'll assume three classes of objects we might put in our boxes.

Fuel
Fuel
> Plants
Plants
> Bamboo
Bamboo
abstract class Fuel
case object KiloOfFuel extends Fuel

abstract class Plant extends Fuel
case object KiloOfPlants extends Plant

abstract class Bamboo extends Plant
case object KiloOfBamboo extends Bamboo

Above, the > sign designates a substitutability relationship. Since Fuel > Plants, we can use Plants anywhere that simply requires a Fuel. This is because plant matter is a type of fuel. Similarly, since Bamboo is a type of plant, we can use Bamboo anywhere that requires Plants or Fuel; therefore, Plants > Bamboo and Fuel > Bamboo. This is the subtype relationship that we're so familiar with.

Boxes of Fuels

Since a box of some thing is just a collection of that thing, it makes sense that the subtype/substitutability relationship for Box[T] looks the same as for the fuel types that we put in the boxes:

Box of Fuel
Box of Fuel
> Box of Plants
Box of Plants
> Box of Bamboo
Box of Bamboo
case object BoxOfFuel extends Box[Fuel](KiloOfFuel)
case object BoxOfPlant extends Box[Plant](KiloOfPlant)
case object BoxOfBamboo extends Box[Bamboo](KiloOfBamboo)

Consumers of Fuels

We'll also assume three classes of objects that might want to do something with our boxes (or, rather, the contents of the boxes).

Fire
Consumer of anything that burns
< Herbivore
Consumers of plants
< Panda
Consumers of bamboo
abstract class Consumer[-U] {
  def apply(box: Box[U]): Unit
}

case object Panda extends Consumer[Bamboo] {
  def apply(box: Box[Bamboo]) = println("Munch munch...")
}

case object Herbivore extends Consumer[Plant] {
  def apply(box: Box[Plant]) = println("Om nom nom...")
}

case object Fire extends Consumer[Fuel] {
  def apply(box: Box[Fuel]) = println("Crackle crackle...")
}

Interestingly, the substitutability relationship goes in the opposite direction here. Although a panda is a type of herbivore, we see that Herbivore < Panda. This is because, although pandas are a subset of the herbivores, the panda's dietary restrictions prevent us from substituting it for a general consumer of all plants. However, since the class herbivores should be able to eat any kind of plant, we could substitute a consumer of plants anywhere that we need a consumer of bamboo.

Covariance of Producers

Boxes are producers because they just provide read-only data for some other object to read (or "consume"). A Box[Bamboo] can be substituted for a Box[Plant] since a Box[Bamboo] is a Box[Plant]. Similarly, a Box[Bamboo] is a Box[Fuel], and a Box[Plants] is a Box[Fuel].

Notice that the "is a" relationship for Box[A] and Box[B] is the same relationship as for type A and type B. Therefore, read-only types like Boxes should use the +covariant notation: Box[+T].

if you need to feed a...
you can give it a...
Fire Box<Bamboo> Box<Plant> Box<Fuel>
Herbivore Box<Bamboo> Box<Plant>
Panda Box<Bamboo>

Contravariance of Consumers

Our Consumer types are obviously consumers. They consume the data provide by a Box. Note that our Consumer types are write-only; i.e., the data goes in, but none comes out. That's the opposite of our read-only Box types. Similarly, the substitutability relationship for Consumers is the opposite as for Boxes; e.g., a Consumer[Plant] is a Consumer[Bamboo]. I can substitute a Fire anywhere I can use a Panda, but I can't substitute a Panda anywhere I can use a Herbivore.

Notice that the "is a" relationship for Consumer[A] and Consumer[B] is the opposite relationship as for type A and type B. Therefore, write-only types like Consumers should use the -contravariant notation: Consumer[-U].

if you have a...
you can give it to...
Box<Bamboo> Fire Herbivore Panda
Box<Plant> Fire Herbivore
Box<Fuel> Fire

Invariant Types

Some types support both reading and writing. A common example is the Array[T] type:

val myArray: Array[Int] = new Array(1, 2, 3)
val x = myArray(0) + 5  // Reading from myArray at index 0
myArray(0) = x          // Writing to myArray at index 0

For types that are both readable and writable, we need to take the intersection of both the "is a" substitutability rules for covariance and contravariance. The intersection of the supertypes of a type T and the subtypes of a type T is only the exact type T. That is why Array[T] is invariant in Scala, and has no +/- annotation on the type parameter. An Array[Plant] is neither an Array[Fuel] nor an Array[Bamboo]. An Array[Plant] is only an Array[Plant].


The images in this document are licensed under the Creative Commons Share-Alike License. The producer/consumer figures were adapted from a wonderful diagram by Andrey Tyukin.