I only found one good and simple explanation of generics Contravariance in Julien Richard-Foy’s blog:
http://julien.richard-foy.fr/blog/2013/02/21/be-friend-with-covariance-and-contravariance/
However it was not clear enough for me to memorize all these details, so I decided to develop the idea in a bit different key.
Let’s define some classification of animals:
Covariance
The easy example of covariance is a group of animals of the same kind (flock, heard, whatever...)
So the group of cows is also a group of ungulate, which is group of mammals, whch is a also group of animals:
Cow -> Ungulate -> Mammal -> Animal
Group[Cow] -> Group[Ungulate] -> Group[Mammals] -> Group[Animals]
This is covariance where Cow is a subclass of Animal, the Group[of Cow] is a (subtype of) Group[of Animal]
Contravariance
Imagine that there’s a law describing Veterinary licensing. The law defines the hierarchy of animal classes as above.
The imaginable law states that a Veterinarian with a license of some class is allowed to treat animals of subclasses of this class of animals, therefore:
LicensedVet[for Cows] can only treat Cows
LicensedVet[for Cattle] can only treat Sheep and Cows
LicensedVet[for Mammals] can only treat Dogs, Cats, Sheep and Cows
LicensedVet[for Animals] can treat anyone from this hierarchy
Cow -> Cattle -> Mammal -> Animal
LicensedVet[Cow] <- LicensedVet[Ungulate] <- LicensedVet[Mammal] <- LicensedVet[Animals]
This is contravariance here: Cow is a subclass of Animal, but LicensedVeterinarian[for cows] is not a (subtype of) LicensedVeterinarian[for all Animals], however the LicensedVeterinarian[for Animals] is a (kind of subtype of) LicensedVeterinarian[for Cows].
This is a practical example of contravariance “from the real life” with kind of practical application.
Ok, let’s do some coding (in Scala):
class Animal(val name: String) {
override def toString = this.getClass.getSimpleName+": "+name
}
class Mammal(name: String) extends Animal(name)
class Dog(name: String) extends Mammal(name)
class Cat(name: String) extends Mammal(name)
class Cattle(name: String) extends Mammal(name)
class Sheep(name: String) extends Cattle(name)
class Cow(name: String) extends Cattle(name)
class Bird(name: String) extends Animal(name)
class Goose(name: String) extends Bird(name)
class Chicken(name: String) extends Bird(name)
class GroupOfAnimals[A](val list: List[A]) {
def getOne = list.head
def getByName(name: String) = list.find({
case p: Animal if p.name == name => true case _ => false }
)
}
class LicensedVeterinarian[A] {
def treat(a: A) = {
print("Treat animal ")
print(a)
}
}
val dorothySheep = new Sheep("Dorothy")
val murkaCow = new Cow("Murka")
val gavCat = new Dog("Gav")
val unnamedHen = new Chicken("Hen")
val flockOfSheep = new GroupOfAnimals[Sheep](List(dorothySheep))
val someCattle = new GroupOfAnimals[Cattle](List(murkaCow, dorothySheep))
val jamesHerriot = new LicensedVeterinarian[Animal]
val bobbySmith = new LicensedVeterinarian[Cattle]
flockOfSheep.getOne //Sheep: Dorothy
someCattle.getOne //Cow: Murka
someCattle.getByName("Murka") //Some(Cow: Murka)
//mr. Herriot can treat anyone
jamesHerriot.treat(dorothySheep)
jamesHerriot.treat(gavCat)
jamesHerriot.treat(unnamedHen)
//Bobby licensed only for Cattle
bobbySmith.treat(dorothySheep)
//bobbySmith.treat(gavCat) //type mismatch, expected: Cattle, actual: Cat
//bobbySmith.treat(unnamedHen) //expected: Cattle, actual: Chicken
Everything seems to work as expected, what for do we need all this covariance and contravariance things?
Imagine that vet can treat a group of animals:
class LicensedVeterinarian[A] {
def treat(a: A) = {
print("Treat animal ")
print(a)
}
def treatGroupOfAnimals(g: GroupOfAnimals[A]) = {
print("Treat animals ")
print(g)
}
}
jamesHerriot.treatGroupOfAnimals(someCattle)
If we try to compile the code with James Herriot doing treatGroupOfAnimals compiler will sipt something like:
type mismatch, expected: GroupOfAnimal[Animal], actual: GroupOfAnimal[Cattle]
What? A flock of cattle is not a group of animals? It happens because parametrized types are non-variant by default, so we must define whether it Covariant or Contravariant, otherwise it is considered to be non-variant.
Group of animals is
covariant to animals, as mentioned above. So, if we redefine group of animals as follows, James Herriot will be able to treat a group of animals:
class GroupOfAnimals[+A](val list: List[A]) {
def getOne = list.head
def getByName(name: String) = list.find({
case p: Animal if p.name == name => true case _ => false }
)
}
jamesHerriot.treatGroupOfAnimals(someCattle) //now works as expected
Ok. What’s the use for contravariance? Let’s imagine that we have a farm:
object Farm {
// This one will not work without covariance of GroupOfAnimals
def areAnimalsOk(group: GroupOfAnimals[Mammal]): Boolean = {
// Dunno am not a vet, they're always lookin fine
true
}
def inviteVetForCattle(veterinarian: LicensedVeterinarian[Cattle]): Unit = {
println("Inviting veterinarian for cattle")
}
def inviteVetForDog(veterinarian: LicensedVeterinarian[Dog]): Unit = {
println("Inviting veterinarian for dog")
}
def inviteVetForBirds(veterinarian: LicensedVeterinarian[Bird]): Unit = {
println("Inviting veterinarian for birds")
}
}
Farm.areAnimalsOk(flockOfSheep) //True: They are always OK
Farm.inviteVetForCattle(bobbySmith)
// ^ This one is OK, Bobby is veterinarian for Cattle
Farm.inviteVetForCattle(jamesHerriot)
//expected:LicensedVeterinarian[Cattle],actual:LicensedVeterinarian[Animal]
Weird! Bobby can be invited to treat Cattle, but James Herriot cannot. As we said above, licensed veterinarians are
contravariant to animals, so let’s try make them contravariant:
class LicensedVeterinarian[-A] {
def treat(a: A) = {
print("Treat animal ")
print(a)
}
def treatGroupOfAnimals(g: GroupOfAnimals[A]) = {
print("Treat animals ")
print(g)
}
}
Farm.inviteVetForCattle(jamesHerriot)
Phew… With contravariance option we can invite James Herriot. Our farm is safe now.
The final code:
class Animal(val name: String) {
override def toString = this.getClass.getSimpleName+": "+name
}
class Mammal(name: String) extends Animal(name)
class Dog(name: String) extends Mammal(name)
class Cat(name: String) extends Mammal(name)
class Cattle(name: String) extends Mammal(name)
class Sheep(name: String) extends Cattle(name)
class Cow(name: String) extends Cattle(name)
class Bird(name: String) extends Animal(name)
class Goose(name: String) extends Bird(name)
class Chicken(name: String) extends Bird(name)
class GroupOfAnimals[+A](val list: List[A]) {
def getOne = list.head
def getByName(name: String) = list.find({
case p: Animal if p.name == name => true case _ => false }
)
}
class LicensedVeterinarian[-A] {
def treat(a: A) = {
print("Treat animal ")
print(a)
}
def treatGroupOfAnimals(g: GroupOfAnimals[A]) = {
print("Treat animals ")
print(g)
}
}
val dorothySheep = new Sheep("Dorothy")
val murkaCow = new Cow("Murka")
val gavCat = new Dog("Gav")
val unnamedHen = new Chicken("Hen")
val flockOfSheep = new GroupOfAnimals[Sheep](List(dorothySheep))
val someCattle = new GroupOfAnimals[Cattle](List(murkaCow, dorothySheep))
val jamesHerriot = new LicensedVeterinarian[Animal]
val bobbySmith = new LicensedVeterinarian[Cattle]
flockOfSheep.getOne //Sheep: Dorothy
someCattle.getOne //Cow: Murka
someCattle.getByName("Murka") //Some(Cow: Murka)
//mr. Herriot can treat anyone
jamesHerriot.treat(dorothySheep)
jamesHerriot.treat(gavCat)
jamesHerriot.treat(unnamedHen)
//Bobby licensed only for Cattle
bobbySmith.treat(dorothySheep)
//bobbySmith.treat(gavCat) //type mismatch, expected: Cattle, actual: Cat
//bobbySmith.treat(unnamedHen) //expected: Cattle, actual: Chicken
jamesHerriot.treatGroupOfAnimals(someCattle)
object Farm {
// This one will not work without covariance of GroupOfAnimals
def areAnimalsOk(group: GroupOfAnimals[Mammal]): Boolean = {
// Dunno am not a vet, they're always lookin fine
true
}
def inviteVetForCattle(veterinarian: LicensedVeterinarian[Cattle]): Unit = {
println("Inviting veterinarian for cattle")
}
def inviteVetForDog(veterinarian: LicensedVeterinarian[Dog]): Unit = {
println("Inviting veterinarian for dog")
}
def inviteVetForBirds(veterinarian: LicensedVeterinarian[Bird]): Unit = {
println("Inviting veterinarian for birds")
}
}
Farm.areAnimalsOk(flockOfSheep)
Farm.inviteVetForCattle(bobbySmith)
Farm.inviteVetForCattle(jamesHerriot)
Farm.inviteVetForDog(jamesHerriot)
Farm.inviteVetForBirds(jamesHerriot)
//Bobby is only licensed for cattle, not for birds or dogs:
//Farm.inviteVetForDog(bobbySmith) // Nope, type mismatch
//Farm.inviteVetForBirds(bobbySmith // Nope, type mismatch