Covariance and contravariance - simple explanation

This is a very concise tutorial on covariance and contravariance. In 10 minutes you should understand what these concepts are and how to use them. The examples are in Scala, but apply to Java or C# as well.

Covariance

Assuming Apple is a subclass of Fruit, covariance lets you treat say List[Apple] as List[Fruit].

val apples = List(new Apple(), new Apple())
processList(apples)

def processList(list:List[Fruit]) = {
  // read the list
}

This seems obvious - indeed, a list of apples is a list of fruit, right?

The surprise comes when we find out this does not work for arrays. Why is that so? Because you could do the following:

val a = Array(new Apple(), new Apple())
processArray(a)

def processArray(array:Array[Fruit]) = {
  array(1) = new Orange() // putting an Orange into array of Apples!
}

The main difference between List and Array here is that the List is immutable (you cannot change its contents) while the Array is mutable. As long as we are dealing with immutable types, everything is OK (as in the first example).

So how does the compiler know that List is immutable? Here is the declaration of List:

sealed abstract class List[+A]

The +A type parameter says "List is covariant in A". That means the compiler checks that there is no way to change contents of the List, which eliminates the problem we had with arrays.

Simply put, a covariant class is a class from which you can read stuff out, but you can't put stuff in.


Contravariance

Now when you already understand covariance, contravariance will be easier - it is exactly the opposite in every sense.

You can put stuff in a contravariant class, but you can never get it out (imagine a Logger[-A] - you put stuff in to be logged). That doesn't sound too useful, but there is one particularly useful application: functions. Say you've got a function taking Fruit:

// isGoodFruit is a func of type Fruit=>Boolean
def isGoodFruit(f:Fruit) = f.ageDays < 3

and filter a list of Apples using this function:

val list:List[Apple] = List(new Apple(), new Apple())
list.filter(isGoodFruit) // filter takes a func Apple=>Boolean

So a function on Fruits is a function on Apples - the filter will throw Apples in and isGoodFruit will know how to handle them.

The type of isGoodFruit is actually Function[Fruit, Boolean] - yes, in Scala even functions are traits, declared as:

trait Function[-A,+B]

So functions are contravariant in their parameter types and covariant in their return types.

OK, that's it; this is the minimal explanation I wanted to cover.

Posted by Martin Konicek on 11:39 PM

8 comments:

tres said...

I am learning scala and find a simple command line demonstration of subtle idea very motivating.
I think I will close the 852 page book for a while and play.
Thanks for the posting.

-- Mike Treseler

shogg said...

class Juice
class MysticJuice extends Juice
class Foodstuff
class Fruit extends Foodstuff

class Squeezer {
Juice squeeze(Fruit)
}
class SpecialSqueezer extends Squeezer {
MysticJuice squeeze(Foodstuff)
}

Covariance/Contravariance is about type safety. What are type-compatible changes/variations when subclassing/specializing an API?

In my example there is a Squeezer and a SpecialSqueezer. SpecialSqueezer has widened the squeeze input parameter type from Fruit to Foodstuff. You can still put Fruits in the SpecialSqueezer. So contravariant changes are type-safe for input types.

SpecialSqueezer has narrowed the squeeze return type from Juice to MysticJuice. No problem since you can treat MysticJuice as Juice. Conclusion: covariant changes are type-safe for output types.

Martin Konicek said...

@Mike: You are welcome. 852 pages is a lot. I would skim through it and read only the most interesting parts. It seems to me that some books could be made much shorter while providing the same information if the author really tried to be concise.
Maybe you will also like this: http://aperiodic.net/phil/scala/s-99/

Anonymous said...

man I love this explanation, very clear,concise and simple language, thanks so much...I hope many future post about the scala "hard parts"..thanks!!!

Anonymous said...

Thanks, so useful!

Anonymous said...

Years old and still one of the better explanations on the internet. I like in particular the +A and -A annotation of the two, it makes it much more readable than Java's ? super A and ? extends A

Post a Comment