DEV Community

Mircea Sirghi
Mircea Sirghi

Posted on

Scala's Variance

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 it is 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 _ =>
    }
  }
} 
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 _ =>
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)