37

I believe one can define covariance (at least, for objects) as 'the ability to use a value of a narrower (sub) type in place of a value of some wider (super) type', and that contravariance is the exact opposite of this.

Apparently, Scala functions are instances of Function[-A1,...,+B] for contravariant parameter types A1, etc. and covariant return type, B. While this is handy for subtyping on Functions, shouldn't the above definition mean I can pass any supertypes as parameters?

Please advise where I'm mistaken.

Jacek Laskowski
  • 72,696
  • 27
  • 242
  • 420
bjt38
  • 533
  • 4
  • 8

5 Answers5

67

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 Bs (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.)

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • Thank you, that was very clear. Attempting to (re)define: 'co/contra-variance are properties dictating the subtype relation between types, subject to the nature of the same relation between their component types'. Abstract, I know, but I prefer to have a definition devoid of examples (although yours was very helpful). – bjt38 May 15 '12 at 17:08
  • thanks for the -A, could you explain why is +B in the Function1[-A,+B] – liango Dec 31 '15 at 14:19
  • 1
    @liango - Anexplanation by example: if you return a `String` you certainly return an `Object`, so `... => String` must be a subclass of `... => Object`. That's what `+B` signifies. – Rex Kerr Dec 31 '15 at 21:19
36

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.

csjacobs24
  • 775
  • 6
  • 13
  • How can I check <: programmatically with those examples you gave? – Captain Obvious Jan 09 '17 at 15:58
  • 1
    @CaptainObvious: Create a method that takes a `Function[Cat, Dog]` as an argument. Try to pass in a `Function[Cat, Animal]` and you'll get a compiler error. Try to pass in a `Function[Calico, Animal]` and you'll also get a compiler error. Those aren't subclasses of `Function[Cat, Dog]`. Then try to pass in a `Function[Animal, Dog]` and that'll work fine. Try to pass in a `Function[Cat, Husky]` and that'll also work fine. Those _are_ subclasses of the `Function[Cat, Dog]`. – csjacobs24 Feb 27 '20 at 17:26
  • Very good explanation! May be considered as the answer for both `variance` and `Liskov principle` topics – Mitrakov Artem Apr 16 '21 at 12:30
6

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.

Asumu Takikawa
  • 8,447
  • 1
  • 28
  • 43
1

A simplified explanation

class A
class B extends A
val printA: A => Unit = { a => println("Blah blah blah") }
printA(new A())   //"Blah blah blah"
printA(new B())   //"Blah blah blah"

contravariance rule:

If B is a subtype of A, then printA[A] is a subtype of printA[B]

Since printA[B] is the superclass, we can use printA(new B())

Zhenyi Lin
  • 111
  • 1
  • 5
-1

Covariant means converting from wider (super) to narrower (sub). For example, we have two class: one is animal (super) and the other one is cat then using covariant, we can convert animal to cat.

Contra-variant is just the opposite of covariant, which means cat to animal.

Invariant means it's unable to convert.

kiritsuku
  • 52,967
  • 18
  • 114
  • 136