Search code examples
scalacontravariance

Contravariant why Foo[C] is a subtype of Foo[B]?


I am trying to understand variance and in the book that I bought it explains as follow:

• A type with an unannotated parameter Foo[A] is invariant in A. This means there is no relationship between Foo[B] and Foo[C] no matter what the sub- or super-type relationship is between B and C.

• A type with a parameter Foo[+A] is covariant in A. If C is a subtype of B, Foo[C] is a subtype of Foo[B].

• A type with a parameter Foo[-A] is contravariant in A. If C is a supertype of B, Foo[C] is a subtype of Foo[B].

I have trouble to understand this sentence:

If C is a supertype of B, Foo[C] is a subtype of Foo[B].

Why not:

Foo[C] is a supertype of Foo[B].

C is a supertype B but why C change suddenly to subtype of B in contravariant?


Solution

  • C is a supertype B but why C change suddenly to subtype of B in contravariant?

    That is the definition of contravariance, it reverses the relation order (in our case, the "is subtype of" relation <:). Notice that it is not that C is now a subtype of B, that relationship is fixed, it is the thing that is a container of C, i.e. Foo[C], is now a subtype of the container of B, Foo[B], not directly B itself.

    The classic example for contravariance is function objects. Functions in Scala are contravariant in their argument type and covariant in their return type, i.e. Function1[-T, +R].

    Lets see an example. Assume we have a small ADT:

    sealed trait Animal
    case class Mouse() extends Animal
    case class Lion() extends Animal
    

    And now we want to create a function from Lion => String. Can we feed it a concrete function from Animal => String?

    def main(args: Array[String]): Unit = {
      val animalToString: (Animal) => String = an => an.toString
      val lionToString: (Lion) => String = animalToString
    
      lionToString(new Lion())
    }
    

    Why does this compile? Because when you invoke lionToString with a Lion, you know for sure that it'll be able to invoke any function defined on Animal, because Lion <: Animal. But the other way around isn't true. Assume Function1 was covariant in its argument type:

    def main(args: Array[String]): Unit = {
      val lionToString: (Lion) => String = an => an.toString
      val animalToString: (Animal) => String = lionToString
    
      lionToString(new Mouse()) // <-- This would blow up.
    }
    

    Then we'd be able to pass a different subtype of Animal when our function actually expects a Lion.