Why is Function[-A1,...,+B] not about allowing any supertypes as parameters?
Covariance and contravariance are qualities of the class not qualities of the parameters. (They are qualities that depend on the parameters, but they make statements about the class.)
So, Function1[-A,+B]
means that a function that takes superclasses of A
can be viewed as a subclass of the original function.
Let's see this in practice:
class A
class B extends A
val printB: B => Unit = { b => println("Blah blah") }
val printA: A => Unit = { a => println("Blah blah blah") }
Now suppose you require a function that knows how to print a B
:
def needsB(f: B => Unit, b: B) = f(b)
You could pass in printB
. But you could also pass in printA
, since it also knows how to print B
s (and more!), just as if A => Unit
was a subclass of B => Unit
. This is exactly what contravariance means. It doesn't mean you can pass Option[Double]
into printB
and get anything but a compile-time error!
(Covariance is the other case: M[B] <: M[A]
if B <: A
.)
This question is old, but I think a clearer explanation is to invoke the Liskov Substitution Principle: everything that's true about a superclass should be true of all its subclasses. You should be able to do with a SubFoo everything that you can do with a Foo, and maybe more.
Suppose we have Calico <: Cat <: Animal, and Husky <: Dog <: Animal. Let's look at a Function[Cat, Dog]
. What statements are true about this? There are two:
(1) You can pass in any Cat (so any subclass of Cat)
(2) You can call any Dog method on the returned value
So does Function[Calico, Dog] <: Function[Cat, Dog]
make sense? No, statements that are true of the superclass are not true of the subclass, namely statement (1). You can't pass in any Cat to a Function that only takes Calico cats.
But does Function[Animal, Dog] <: Function[Cat, Dog]
make sense? Yes, all statements about the superclass are true of the subclass. I can still pass in any Cat -- in fact I can do even more than that, I can pass in any Animal -- and I can call all Dog methods on the returned value.
So A <: B
implies Function[B, _] <: Function[A, _]
Now, does Function[Cat, Husky] <: Function[Cat, Dog]
make sense? Yes, all statements about the superclass are true of the subclass; I can still pass in a Cat, and I can still call all Dog methods on the returned value -- in fact I can do even more than that, I can call all Husky methods on the returned value.
But does Function[Cat, Animal] <: Function[Cat, Dog]
make sense? No, statements that are true of the superclass are not true of the subclass, namely statement (2). I can't call all methods available on Dog on the returned value, only the ones available on Animal.
So with a Function[Animal, Husky]
I can do everything I can do with a Function[Cat, Dog]
: I can pass in any Cat, and I can call all of the Dog methods on the returned value. And I can do even more: I can pass in other animals, and I can call of the methods available on Husky that aren't available on Dog. So it makes sense: Function[Animal, Husky] <: Function[Cat, Dog]
. The first type parameter can be replaced with a superclass, the second with a subclass.
There are two separate ideas at work here. One is using subtyping to allow more specific arguments to be passed to a function (called subsumption). The other is how to check subtyping on functions themselves.
For type-checking the arguments to a function, you only have to check that the given arguments are subtypes of the declared argument types. The result also has to be a subtype of the declared type. This is where you actually check subtyping.
The contra/co-variance of the parameters & result only factor in when you want to check whether a given function type is a subtype of another function type. So if a parameter has type Function[A1, ... ,B]
, then the argument has to be a function type Function[C1, ..., D]
where A1 <: C1 ...
and D <: B
.
This reasoning isn't specific to Scala and applies to other statically-typed languages with subtyping.