Intro
I think Scala variance chapter is misunderstood heavily even by skillful programmers. All the articles I have found so far explain more or less the same boilerplate they may have found on some common resource and everybody repeat themselves and don't cover enough aspects which makes everybody confuse this concept.
Understanding Contravariance
This appears to be the hardest, however, it is not harder then Covariance, even more, it is easier that Covariance. IMHO Covariance is misunderstood as well, which makes Contravariance so difficult.
Notation: SomeClass[-A] - reads as SomeClass is contravariant in A
How is it used ?
The widespread example is on Serializers. If one defines a Serializer on a [parent of A] it can further be assigned to a Serializer of [A]. This way, it is not necessary to -reinvent the wheel-. In my words, I define something for a [parent of A] and I can use it for [A]. So it works at the class level.
Still, what's the point ?
It is like kibble that can be used to feed Dogs and Cats. Same food, different Animals.
abstract class Food {
val foodType: String
}
abstract class Pet(val name: String)
class Dog(override val name: String) extends Pet(name)
class Cat(override val name: String) extends Pet(name)
class Kibble[-A](override val foodType: String)
extends Food {
def printData(values: List[A]): Unit = {
for (item <- values) {
item match
case p: Pet =>
println(p.getClass.getSimpleName + " "
+ p.name + " food type is "
+ foodType)
case _ =>
}
}
}
So the following apply:
def main(args: Array[String]): Unit = {
val listCat: List[Cat] = List(Cat("cat1"), Cat("cat2"))
val listDog: List[Dog] = List(Dog("dog1"), Dog("dog2"))
var kibble = Kibble[Pet]("pelletsOfMeat")
var kibbleCat: Kibble[Cat] = kibble
var kibbleDog: Kibble[Dog] = kibble
kibbleCat.printData(listCat)
kibbleDog.printData(listDog)
kibble.printData(listCat)
kibble.printData(listDog)
}
Implications
First implication:
SomeClass of [A] can be assigned a value of a SomeClass of a [parent of A or A].
Second implication:
One can create list Kibble[Cat] and Kibble[Dog] just from Kibble[Pet] without having to dig into specifics.
Third implication:
A function over Pet type can be passed a Cat type and it will run successfully. Also it will have a strong type checking.
Understanding Covariance
So, what is Covariance ? Well, it is not easier that Contravariance, and it's purpose is not opposed, it's different.
Notation: SomeClass[+A] - reads as SomeClass is covariant in A
How is it used ?
The widespread example is of a function that is consuming a parent and is passed a child and it still works. So it works at function level as opposed to Contravariance that works at class level. However, it works at class level as well. So, there are two variations of Covariance.
Variation 1, Class level
abstract class Food {
val foodType: String
}
class Kibble[+A](override val foodType: String, val pets: List[A]) extends Food {
def printData: Unit = {
for (item <- pets) {
item match
case p: Pet =>
println(p.getClass.getSimpleName + " "
+ p.name + " food type is "
+ foodType)
case _ =>
}
}
}
abstract class Pet(val name: String)
class Dog(override val name: String) extends Pet(name)
class Cat(override val name: String) extends Pet(name)
The following apply:
def main(args: Array[String]): Unit = {
val listCat: List[Cat] = List(Cat("cat1"), Cat("cat2"))
val listDog: List[Dog] = List(Dog("dog1"), Dog("dog2"))
var kibbleCat: Kibble[Cat] = Kibble[Cat]("pelletsOfMeat", listCat)
var kibbleDog: Kibble[Dog] = Kibble[Dog]("pelletsOfMeat", listDog)
var kibbleCat1: Kibble[Pet] = Kibble[Cat]("pelletsOfMeat", listCat)
var kibbleDog1: Kibble[Pet] = Kibble[Dog]("pelletsOfMeat", listDog)
kibbleCat.printData
kibbleDog.printData
kibbleCat1.printData
kibbleDog1.printData
}
Variation 2, function level
def printDataVariation(kibble: Kibble[Pet]): Unit = {
for (item <- kibble.pets) {
item match
case p: Pet =>
println(p.getClass.getSimpleName + " "
+ p.name + " food type is "
+ kibble.foodType)
case _ =>
}
}
The following apply:
def main(args: Array[String]): Unit = {
val listCat: List[Cat] = List(Cat("cat1"), Cat("cat2"))
val listDog: List[Dog] = List(Dog("dog1"), Dog("dog2"))
var kibbleCat: Kibble[Cat] = Kibble[Cat]("pelletsOfMeat", listCat)
var kibbleDog: Kibble[Dog] = Kibble[Dog]("pelletsOfMeat", listDog)
printDataVariation(kibbleCat)
printDataVariation(kibbleDog)
}
Implications
First implication:
SomeClass of [A] can be assigned a value of a SomeClass of a [child of A or A].
Second implication:
A function that works with Pet type will work with child of Pet type.
Invariance
Will elaborate on Invariance later. At this point, it feels straight forward. Will work on it if I observe any interesting elements.
Top comments (0)