Monday 19 October 2015

Practical generic types Contravariant and Covariant subtyping in Scala

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



No comments:

Post a Comment