top of page
Writer's picturePranay Kundu

Covariance, Contravariance and Invariance - Functional Programming with Scala - 101 Series

Updated: Jan 16, 2021

I am back with another interesting and well-known and most googled topic, Covariance, Contravariance and Invariance. And I know, the articles out there are really tough to understand and I have been there. Let me try making things simpler for the world.


Covariance, Contravariance
Source: StackOverflow

For the students looking for a quick definition for your exams, here's the one:

Ok! For people looking for more, Let's dwell into details and easy discussion!


What is a Variance?


In simple words, it is the relationship between Subtypes and Genericity. How classes, their subclasses and their superclasses behave when defined for generic classes. In Java and C++, you may be familiar with generic class definitions in the form of:

class Test<T> 

Same we will be exploring in scala for generic classes and their relationship with subtypes. In scala, we define generic classes in either of the form:

class Foo[+A] //A covariant class
class Bar[-A] //A contravariant class
class Baz[A]  //An invariant class


Covariance


Let's build on the simple definition -

In this definition, List is an abstract class defined for a type T. Something like this:

abstract class List[+T]

This makes List a Generic Class which can be of moulded to any type. List in scala by definition covariant in nature. Trust me! Covariance is simplest to understand and most intuitive.

abstract class BreakingBadCharacter {
  def name: String
}
case class MaleCharacter(name: String) extends BreakingBadCharacter
case class FemaleCharacter(name: String) extends BreakingBadCharacter

Let me define a function which takes List[BreakingBadCharacter] -

def sayMyName(characters: List[BreakingBadCharacter]): Unit =
  characters.foreach { character =>
    println(s"My Name is ${character.name}")
  }

Now we define the List for male and female characters and let's see how our covariant List behaves when we pass these subtype List.

val maleCharacters: List[MaleCharacter] = List(
  MaleCharacter("Heisenberg"),
  MaleCharacter("Jesse Pinkman"),
  MaleCharacter("Gus Fring")
)

val femaleCharacters: List[FemaleCharacter] =
  List(FemaleCharacter("Skyler White"), FemaleCharacter("Marie Schrader"))

println("First printing Male characters!")
sayMyName(maleCharacters)

println("Second printing Female characters!")
sayMyName(femaleCharacters)

So here's the output:

First printing Male characters!
My Name is Heisenberg
My Name is Jesse Pinkman
My Name is Gus Fring
Second printing Female characters!
My Name is Skyler White
My Name is Marie Schrader

You can get this code on my Github.

Let's try to define the same function for List[MaleCharacter] and let's see if it does what it is supposed to do!

def sayMyName(characters: List[MaleCharacter]): Unit =
  characters.foreach { character =>
    println(s"My Name is ${character.name}")
  }
  
val femaleCharacters: List[BreakingBadCharacter] =
  List(
    new BreakingBadCharacter {
      override def name: String = ("Skyler White")
    },
    new BreakingBadCharacter {
      override def name: String = "Marie Schrader"
    }
  )

sayMyName(femaleCharacters)

And as expected it throws a compilation Error -

type mismatch;
 found   : List[BreakingBadCharacter]
 required: List[MaleCharacter]
    sayMyName(femaleCharacters)

Usage


The covariance is heavily used in the cases where we extend the functionality of a class to its subtype. It helps in reusing the same code for both parent and subclasses/types. It's one of the most intuitive and implemented for most of the pre-defined classes and immutable collections in scala.



Invariance


Another intuitive and easy approach to generic class definitions. Let's look at its simple definition:

In scala lib, we already have mutable collections like Array, ArrayBuffer, ListBuffer etc. defined as an invariant class but also some immutable collection like Set.


Let's look into an example -

class DEA[T <: BreakingBadCharacter](agent: T) {
  def catchTheGuys = s"I am ${agent.name} from DEA, I catch bad guys"
}

class CivilianInAlbuquerque[T <: BreakingBadCharacter](character: T) {
  def mindingMyBusiness = s"I am ${character.name}, just mind my own business!"
}

case class DEAAgent(name: String) extends BreakingBadCharacter

So here we have defined two classes to show the invariance behaviour. We define two functions to show the strict typing of Invariant classes for the subtype.

def thisIsMyJobAsCop(cop: DEA[DEAAgent]): Unit = {
  println(cop.catchTheGuys)
}

def thisIsMyJobAsCivilian(
    civilian: CivilianInAlbuquerque[BreakingBadCharacter]
): Unit = {
  println(civilian.mindingMyBusiness)
}

Now we set our DEAAgent and Civilian for the test:

val hank = DEAAgent("Hank Schrader")
val heisenberg = new BreakingBadCharacter {
  override def name: String = "Heisenberg"
}

So when we do the expected, it's all sunny but when you do some illegal moves... Invariance comes into the picture.

thisIsMyJobAsCop(new DEA(hank))
>> I am Hank Schrader from DEA, I catch bad guys 
/**
This gives ERROR!
thisIsMyJobAsCop(new DEA(heisenberg))

As you can see its a strict Typing, Heisenberg cannot become a part of DEA as cop!
**/

thisIsMyJobAsCivilian(new CivilianInAlbuquerque(heisenberg))
>> I am Heisenberg, just mind my own business!
/**
This gives ERROR!
thisIsMyJobAsCivilian(new CivilianInAlbuquerque[DEAAgent](hank))

type mismatch;
found   : CivilianInAlbuquerque[DEAAgent]
required: CivilianInAlbuquerque[BreakingBadCharacter]
Note: DEAAgent <: BreakingBadCharacter, but class CivilianInAlbuquerque is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
thisIsMyJobAsCivilian(new CivilianInAlbuquerque[DEAAgent](hank))
**/

However, there's a case which you might find strange but don't be astonished, it's the implicits in place!

// However this works!
// Implicitly Hank is made a BreakingBadCharacter out of its DEAAgent uniform to behave as civilian
thisIsMyJobAsCivilian(new CivilianInAlbuquerque(hank))
>> I am Hank Schrader, just mind my own business!

The code for the same could be found here.


Relationship between mutability and variance


By now we already saw a thought-provoking relationship between mutable collections being invariant and immutable being covariant. Let's take an example of ListBuffer[String] - a mutable invariant collection if it was covariant, then you could assign it to a ListBuffer[Any] and then add Int to that ListBuffer. Suppose the below code compiles(in reality it won't)

scala> val greetings: ListBuffer[String] = ListBuffer("Hello")
greetings: ListBuffer[String] = ListBuffer("Hello")

scala> val anything: ListBuffer[Any] = greetings

scala> anything += 1
res4: anything.type = ListBuffer(1, "Hello")

Basically, you have mutated the ListBuffer[Any] to Any. So, why this is not the case with List - a covariant immutable collection.

scala> val greetings: List[String] = List("Hello")
greetings: List[String] = List("Hello")

scala> val anythings: List[Any] = greetings
everything: List[Any] = List("Hello")

scala> 1 :: greetings
res5: List[Any] = List(1, "Hello")

So basically for immutable collections, it doesn't mutate but create a new one ensuring the type, therefore it can be covariant where original type is not mutated.


Usage


Invariance is used for the classes where you want to impose the strict typing irrespective of the class/type or subclass/type. It's used almost every time and for most of the use cases where type safety is pretty strict or non-negotiable.



Contravariance


Finally to the topic of utter confusion and people question its use beyond academics. You will get to know why it's a difficult topic to understand as it's not so intuitive. Let's look at its simple definition:

So it's just opposite of covariance! Its use in real life is very rare but I think it has its own importance. Scalaz's ordering is one such application.


Let's discuss this more with an example:

class CastStylist[-T] {
  def design: String = "I am designing Costume for"
}

I have defined a contravariant class for the discussion which has a def in it. This def is being used to execute the contravariant demo function -

def designMaleCostumes(
    castStylist: CastStylist[MaleCharacter],
    character: MaleCharacter
): String = s"${castStylist.design} ${character.name}!!

Now let's define our stylist to come into action

val heisenberg = MaleCharacter("Heisenberg")
val maleStylist = new CastStylist[MaleCharacter]
val leadStylist = new CastStylist[BreakingBadCharacter]

Here's the run for the above:

println(designMaleCostumes(maleStylist, heisenberg))
>> I am designing Costume for Heisenberg!!

println(designMaleCostumes(leadStylist, heisenberg))
>> I am designing Costume for Heisenberg!!

If we look into the contravariance behaviour for the def being defined for the parent class, doesn't allow the mentioned operations to not work.

/***
This won't work -

def designCostumes(
  castStylist: CastStylist[BreakingBadCharacter],
  character: BreakingBadCharacter
): String = s"${castStylist.design} ${character.name}!!"

We designed this class(CastStylist) keeping in mind that Stylists are if experts for male dressing(CastStylist[MaleCharacter]) exists, they can be trusted only for male Actors, whereas LeadStylists(CastStylist[BreakingBadCharacter]) can do for both male and female Actors.
 ***/

the code could be found here.


Usage


Contravariance is used for reverse type safety for classes. Especially where the parent class/type can do all things a subtype/class can. It's weird because we inherit a parent class to add functionality or override functionality to a child, very rarely a parent class can do all that a subclass can. Still, it's relevant.


Conclusion

Variance is a great tool to approach class designing and type safety and its relation with inheritance. There's the fourth type of variance which is not so popular and I have not included in the definition. Hopefully, I have covered this complex topic in the most simplified and fun way. Any doubts, comment down below, I will address them. Till then, Happy coding!


502 views0 comments

Comments


bottom of page