Why Is Contravariance in Scala so Hard?
Join the DZone community and get the full member experience.
Join For Freeval list: List[Int] = List(1,2,3)
For your convenience, you can also watch this on YouTube or here in the video below.
So What's Variance?
+
next to the type argument, like
xxxxxxxxxx
abstract class List[+T]
xxxxxxxxxx
val laika: Animal = new Dog("Laika")
val myDogs: List[Animal] = List(lassie, hachi, laika)
This is more easily understood.
Dog
subtype of
Animal
, therefore
List[Dog]
subtype of
List[Animal]
.
Dog
subtype of
Animal
? I don't care.
List[Dog]
and
List[Animal]
are two different types, with no relationship between them. That means if you write
xxxxxxxxxx
val myDogs: List[Animal] = List(lassie, hachi, laika)
the code will not compile because the compiler expects a
List[Animal]
and you're giving it a
List[Dog]
.
Enter Contravariance
Dog
subtype of
Animal
, and we're wondering what could be the relationship between
List[Dog]
and
List[Animal]
. In the contravariance answer, we would have a list of animals being a subtype of a list of dogs, which is the exact opposite of covariance above. The following will be very confusing.
xxxxxxxxxx
class MyList[-T]
val myAnimals: MyList[Dog] = MyList(crocodile, kitty, lassie) // some animals
When you write a minus in the generic type, that's a marker to the compiler that the generic type will have the subtype relationship exactly opposite to the types it wraps. Namely,
MyList[Animal]
is a subtype of a
MyList[Dog]
. The code above would compile, but we would not be happy because it makes no sense. Why would we write that for a list? Why would a list of animals be a SUBTYPE of a list of dogs?
The Why
The Because
xxxxxxxxxx
trait Vet[-T] { // we can also insert an optional -T <: Animal here if we wanted to impose a type constraint
def heal(animal: T): Boolean
}
I've already defined it with a
-T
, for the following reason: if you ask me, "Daniel, gimme a vet for my dog" and I'll give you a vet which can heal ANY animal, not just your dog, your dog will live.
xxxxxxxxxx
val myDog = new Dog("Buddy")
val myVet: Vet[Dog] = new Vet[Animal] { ... }
myVet.heal(myDog)
Vet[Dog]
, and instead we have a
Vet[Animal]
, with the meaning that the vet can heal any animal; therefore, it can work on my dog as well. The code will compile, our buddy will live, and we would be happy.
The Punchline
-
Examples of covariant concepts: a cage (holds animals), a garage (holds cars), a factory (creates objects), a list (and any other collection).
-
Examples of contravariant concepts: a vet (heals animals), a mechanic (fixes cars), a garbage pit (consumes objects), a function (it acts on/it's applied on arguments).
Published at DZone with permission of Daniel Ciocirlan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments