Functions are contravariant in their argument types and co-variant in their return types

3.6k views Asked by At

I have read this and this answers before posting this question, but I am still a bit unclear in my understanding of this topic as explained below:

I understand what covariant and contravariant independently mean.

If I have classes as below:

class Car {}
class SportsCar extends Car {}
class Ferrari extends SportsCar {}

And:

object covar extends App {

    // Test 1: Works as expected

    def test1( arg: SportsCar => SportsCar ) = {
        new SportsCar
    }

    def foo1(arg: Car): Ferrari = { new Ferrari }
    def foo2(arg: SportsCar): Car = { new Ferrari }
    def foo3(arg: Ferrari): Ferrari = { new Ferrari }

    test1(foo1) // compiles
    test1(foo2) // Fails due to wrong return type - violates return type is covariant
    test1(foo3) // Fails due to wrong parameter type - violates param type is contravariant

    // Test 2: Confused - why can I call test2 succesfully with ferrari 
    // as the parameter, and unsuccesfully with a car as the parameter?

    def test2(arg: SportsCar): SportsCar = {
        new Ferrari
    }

    val car = new Car()
    val sportsCar = new SportsCar()
    val ferrari = new Ferrari()

    val f1 = test2(ferrari)  // compiles - why?
    val f2 = test2(car)      // fails - why?

}

As mentioned above, in the Test 2, why can I call test2 succesfully with ferrari as the parameter, and unsuccesfully with a car as the parameter?

Does the statement Functions are contravariant in their argument types and co-variant in their return types only apply to functions which are passed as arguments? I guess I am not making appropriate distinction between the statement and the 2 tests that I wrote.

1

There are 1 answers

0
Yuval Itzchakov On BEST ANSWER

That's because you're mixing subtyping with co/contravariance.

In your first example you expect a Function1[-T, +R]. In that case, the rules of co/contravariance apply to the function type. In your second example, you follow simple subtyping rules.

In test1, where you pass in foo1 which is Car => Ferrari everything works because foo1 expects a Car, but gets a SportsCar, which is a Car. Any method that requires a Car can deal with a subtype because of the subtyping nature. But these rules don't work when we talk about subtyping alone.

If we expand test1 with foo1 with the actual types, perhaps it'll be clearer:

foo1:

  • Argument type:
    • Expectation: Car
    • Actual: SportsCar
  • Return type:
    • Expectation: SportsCar
    • Actual: Ferrari

Everything lines up well.

In test2 the rules change. If I expect a SportsCar you pass in any car, then I can no longer depend on the input being a sports car and everything breaks, but if you pass in a ferrari which is actually a sports car, everything is fine.

Again, let's line up the types:

test2:

  • First case:
    • Argument type:
      • Expectation: SportsCar
      • Actual: Ferrari (subtype)
    • Result: everyone is happy
  • Second case:
    • Argument type:
      • Expectation: SportsCar
      • Actual: Car
    • Result: Error. We're not sure car is an actual sports car.