soupybionics
soupybionics

Reputation: 4390

Contravariance and covariance in Scala

abstract class Bhanu[-A] { val m:List[A] }  

gives

error: contravariant type A occurs in covariant position in type => List[A] of value m
       abstract class Bhanu[-A] { val m:List[A] }

whereas

abstract class Bhanu[+A] { val m:List[A] }

gives

defined class Bhanu

I am not able to wrap my head around this concept as to why it fails for contravariance whereas it succeeds for covariance.

Secondly (from some other example),

What does the statement exactly mean?

Function1[Sport,Int] <: Function1[Tennis,Int] since Tennis <: Sport

It seems counter-intuitive to me, Shouldn't it be the following?

Function1[Tennis,Int] <: Function1[Sport,Int] since Tennis <: Sport

Upvotes: 2

Views: 1133

Answers (2)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149528

dkolmakov's answer does a good job explaining why this particular example won't work. Perhaps a more general explanation would help as well.

What does it mean for a type constructor, a function or a trait to be of variance? According to the definition on Wikipedia:

Within the type system of a programming language, a typing rule or a type constructor is:

  • Covariant: if it preserves the ordering of types (≤), which orders types from more specific to more generic;

  • Contravariant: if it reverses this ordering;

  • Invariant or nonvariant if neither of these applies.

Now, what is an ordering on types? and what in the world does it mean to preserve or reverse the ordering? It means that for any type T and U, there either exists a relationship where:

  • Covariance: T <: U -> M[T] <: M[U] - For example, a Cat <: Animal, so List[Cat] <: List[Animal]
  • Contravariance: T <: U -> M[T] >: M[U] - For example, a Cat <: Animal, so Function1[Cat, Unit] >: Function1[Animal, Unit]

or invariant if there is no relationship between the two.

Notice how covariance preserves the ordering between types, since a Cat derives Animal. Now notice how contravariance reverses the ordering, since now Function0[Animal, Unit] derives a Function0[Cat, Unit].

How can we take this notion of variance to our advantage? Based on these rules we can generalize the assignment compatibility between type constructors! Good examples are List[A], Option[A] and Function1[-T, +U] (or any FunctionN really).

Let's take for example a Function1[-T, +U] (T => U) which has both a covariant and contravariant parameter.

Why is that the input type parameter is contravariant and output type is covariant? First, according to the axioms defined above, we can see that:

Function1[Sport,Int] <: Function1[Tennis,Int]

The input type parameter reverses the relationship on types, since usually, Tennis <: Sport, but here it is opposite. Why is this so? Because any function that takes in a Sport will know how to deal with a Tennis, but the opposite isn't true. For example:

val sportFunc: (Sport => Int) = ???
val tennisFunc: (Tennis => Int) = sportFunc

val result = tennisFunc(new Tennis())

But would a function expecting a Tennis know how to deal with any Sport? Of course not:

val tennisFunc: (Tennis => Int) = ???
val sportFunc: (Sport => Int) = tennisFunc

// The underlying function needs to deal with a Tennis, not a `FootBall`.
val result = sportFunc(new FootBall()) 

The opposite is true regarding the output types which are covariant, Anyone expecting a Sport as a return type can deal with a Tennis, or FootBall, or VollyBall.

Upvotes: 6

dkolmakov
dkolmakov

Reputation: 657

Let's look on the first example you mentioned. Consider we have:

class Fruit
class Apple extends Fruit
class Banana extends Fruit

class Bhanu[-A](val m: List[A]) // abstract removed for simplicity

Since Bhanu is contravatiant Bhanu[Fruit] <: Bhanu[Apple] so you can do the following:

val listOfApples = new List[Apple](...)
val listOfFruits = listOfApples // Since Lists are covariant in Scala 
val a: Bhanu[Fruit] = new Bhanu[Fruit](listOfFruits)
val b: Bhanu[Banana] = a // Since we assume Bhanu is contravariant
val listOfBananas: List[Banana] = b.m
val banana: Banana = listOfBananas(0) // TYPE ERROR! Here object of type Banana is initialized 
                                      // with object of type Apple

So Scala compiler protects us from such errors by restriction to use contravariant type parameters in covariant position.

For your second question let's also look at the example. Consider we have function:

val func1: Function1[Tennis,Int] = ...

If Function1[Tennis,Int] <: Function1[Sport,Int] where Tennis <: Sport as you proposed than we can do the following:

val func2: Function1[Sport,Int] = func1
val result: Int = func2(new Swimming(...)) // TYPE ERROR! Since func1 has argument 
                                           // of type Tennis.

But if we make Function1 contravariant in its argument so Function1[Sport,Int] <: Function1[Tennis,Int] where Tennis <: Sport than:

val func1: Function1[Tennis,Int] = ...
val func2: Function1[Sport,Int] = func1 // COMPILE TIME ERROR!

and everything is fine for the reverse case:

val func1: Function1[Sport,Int] = ...
val func2: Function1[Tennis,Int] = func1 // OK!
val result1: Int = func1(new Tennis(...)) // OK!
val result2: Int = func2(new Tennis(...)) // OK!

Functions must be contravariant in their argument type and covariant in result type:

trait Function1[-T, +U] {
  def apply(x: T): U
}

Upvotes: 6

Related Questions