Contravariance in the real world

Contravariance hurts the brain! The definition is straightforward, but applying it to the real world doesn’t make much sense. How come Container[Waste] be a subtype of Container[Paper] when Paper is a more specialized type of Waste?

The key to understanding contravariance is to stop thinking of types in terms of “is a more specialized type” and switch the focus to the idea of acceptance.

Visualize the following scenario in Scala:

class Waste
class Paper extends Waste
class Glass extends Waste
class Container[T] {
//...
}
object Office {
def setPaperContainer(container: Container[Paper]): Unit = ???
  def setGlassContainer(container: Container[Glass]): Unit = ???
}

The Office only accepts specific types of containers: Container[Paper] and Container[Glass]. But that’s not practical! What happens, in reality, is to have the same type of container with different colors:

Paper vector created by Lexamer — Freepik.com

They are not specialized as the example suggests.

The following expresses better what happens in reality:

object Office {
def setPaperContainer(container: Container[Waste]): Unit = ???
  def setGlassContainer(container: Container[Waste]): Unit = ???
}

But, what if policies in the office require a particular container for Paper that shreds everything? Would it make sense to let Container be covariant? Could the Office accept Container[Waste], or any other more specialized type for both Paper and Glass?

class Container[+T] { //<---- Covariant
//...
}
object Office {
def setPaperContainer(container: Container[Waste]): Unit = ???
  def setGlassContainer(container: Container[Waste]): Unit = ???
}

The answer is NO. Otherwise, the Office would accept a shredder as a Container[Glass] and the result would be messy!

So, how to express that a Container can be either the basic type or only specialized for a given type? In other words, how to accept a Container[Waste] and a Container[Paper] for Paper, and just accept a Container[Waste] and Container[Glass] for Glass?

Using contravariance!

class Container[-T] {//<---- Contravariant
//...
}
object Office {
def setPaperContainer(container: Container[Paper]): Unit = ???
  def setGlassContainer(container: Container[Glass]): Unit = ???
}

By making Container contravariant, the Office accepts a Container[Waste] for both Paper and Glass, or a specialized type for each one. And it doesn’t accept a shredder for Glass, or a specialized Glass container for Paper.

Don’t crush glasses in a paper shredder. Use contravariance!