> 140

Contravariant Traits in Scala or “Help Rahul”

In English, Programming on November 15, 2010 at 9:48 pm

Today I posted a little tweet expressing my happiness about mastering contravariants in scala. Rahul contacted me asking for a little blog post about this (1, 2).

So here is my modest attempt to tell the world about contravariants.
Edit: and it’s “a little bit” longer than I thought it would be😉

If you’re Rahul then this is specially for you.
If you’re not Rahul but interested in contravariants in scala, regard yourself as Rahul and this is specially for you.
Sorry about the “blog”. Guess soup.io wasn’t really cut for comments, posting source code and stuff. But it will do the trick. Feel free to contact me via Twitter if you have any questions. I will try to link them to this post. EDIT: OK this is the reason I moved to wordpress😉

And of we go:
let’s start by defining 3 little classes

class SuperLittleProfessor(val i:Int)

class LittleProfessor(tmp:Int) extends SuperLittleProfessor(tmp) {
def add(x:Int) = i + x
}

class SubLittleProfessor(tmp:Int) extends LittleProfessot(tmp) {
def mult(x:Int) = i * x
}

Pretty straight forward. Let us repeat what inheritance gives us: We can modify or enhance given classes and we can use the new class where ever we would have used the old one. Let me stress this again because we are going to need it: If class B extends class A then we can/want/should be able to use it where ever we would have used or will use A.

Got that?
Good.

Now we will introduce a little trait. I’m starting with an covariant one, because that’s much easier to grasp. We’ll get to a contravariant one later:

trait Creator[+A] {
def create(i:Int):A
}

Again no rocket science here. Now what does the little “+” tell us? It says that if have a type T1 and a subtype T2 that an instance of Creator[T2] is automatically a subtype of of Creator[T1] and we can use it everywhere we would have used Creator[T1].

“Wait a second” I here you say “why can we do that?”.

Well see it like this: if we call create() on an instance of Creator[T1] we are getting a T1. If we call create() on an instance of Creator[T2] we are getting a T2. T2 is a subtype of T1 which can be used everywhere we would have used T1. Hence we can use Creator[T2] where ever we would have used Creator[T1].

Get it?
Lets try it in REPL
scala> class LilProfCreator extends Creator[LittleProfessor] {
|   def create(i:Int) = new LittleProfessor(i)
| }
defined class LilProfCreator

scala> class SubLilProfCreator extends Creator[SubLittleProfessor] {
|   def create(i:Int) = new SubLittleProfessor(i)
| }
defined class SubLilProfCreator

scala> val a = List(new LilProfCreator)
a: List[LilProfCreator] = List(LilProfCreator@61a0353d)

scala> a :+ new SubLilProfCreator
res1: List[ScalaObject with Creator[LittleProfessor]] = List(LilProfCreator@61a0353d, SubLilProfCreator@5655d1b4)

See that? We add a Creator[SubLittleProfessor] to a List[LilProfCreator] (which could be replaced by List[Creator[LittleProfessor]]) and it all worked. In other words: we used a Creator[SubLittleProfessor] where we would have expected a Creator[LittleProfessor].

Nice! And intuitive isn’t it? Believe me: contravariants are as intuitive, once you’re looking at them the right way.
Let’s get into it and start with a contravariant trait:

trait User[-A] {
def use(a:A):Int
}

Now what does the “-” say? It says that User[T2] is a subtype of User[T1] if T2 is a supertype of T1 . . .

WHAAAAAAAAAAAAAAAAAAAT?

Stay calm! Let’s step 3 steps back and take a look at Creator again.
There’s got to be a hint right?
Yes there is.

In Creator the type parameter appeared as result of a function.
In User the type parameter appears as parameter of a function.

Now let’s step back even one more step: I deliberately chose the structure of the LittleProfessor system so that every subclass adds a new feature. We were able to substitute Creator[LittleProfessor] for a Creator[SubLittleProfessor] because SubLittleProfessor is the more advanced type with more features. In a place where we’d expect a LittleProfessor we wouldn’t mind the extra mult() function that SubLittleProfessor offers us.

Now with User and the function use() that receives something from us we have to loosen that constraint!
In other words: In oder to let us use User[T2] where we would expect a User[T1] we have to demand LESS or equal features from T2 than from T1. This way we can feed a T1 to the function use() of User[T2]. It sure won’t mind the extra features. But we can’t feed a T2 to a User[T1].use() since it might demand a feature that T2 doesn’t have.

Or as I put it above: User[T2] is a SUBtype of User[T1] if T2 is a SUPERtype of T1

Lets try it again in REPL

scala> class LilProfUser extends User[LittleProfessor] {
|   def use(a:LittleProfessor) = a add 1
| }
defined class LilProfUser

scala> class SubLilProfUser extends User[SuperLittleProfessor] {
|   def use(a:SuperLittleProfessor) = a.i
| }defined class SubLilProfUser

scala> val b = List(new LilProfUser())
b: List[LilProfUser] = List(LilProfUser@3eb217d5)

scala> b :+ new SuperLilProfUser()
res2: List[ScalaObject with User[LittleProfessor]] = List(LilProfUser@3eb217d5, SuperLilProfUser@5dcdd76a)

So that’s all there is to it. You need to enforce the constraints on type parameters that appear as results and loosen the constraints on type parameters that appear as parameters if you want to benefit from automatic inheritance.

Of course that also implies 2 things: invariant type parameters may only appear as results contravariant type parameters may only appear as parameters.
Otherwise you get a compiler error.
EDIT: This again implies two other things
– a val can only be convariant
– a var can only be invariant since it can’t be convariant and contravariant at the same time

Any questions left?

  1. Thanks for the article. How would you explain the difference between +A and T <: A ?

  2. wow! my very first comment on my very first blog😉

    sorry, that I had to approve your comment first. Changed that setting now.

    As for me explaining stuff: I’m still waiting for Martin Odersky to show up and tell me that I got it all wrong😉. So whatever I say . . . please cross-check with “more official” sources.
    That being I said, I’ll give it a shot:

    It is my understanding that these two are actually very distinct and have no overlapping.

    Let’s say we have two classes A and B (with B being a subclass of A), some random Trait R and one more interesting Trait T[X].

    Now T[X <: R] introduces a constraint. You can only use subtypes of R as typeparameter. But it does not tell us anything about the relation between T[A] and T[B] i.e. T[B] is NOT a subtype of T[A] (or the other way round)

    On the other hand I like to see T[+A] (and T[-A] too) as "unlocking a feature". This does in no way constrain the types we can use as type parameter but tells us that T[B] will be a subtype of T[A] (of course this "unlocked" feature comes with constraints . . . which I described in the article).

    So you see they have nothing in common and actually you can even use them together. I’m only working in Scala for about 2 month now and I already have encountered at least 3 cases where I wrote a type signature like T[+X <: A]

    Any questions?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s