Variance is not hard and it’s extremely useful

arshad ali sayed
kodeyoga
Published in
6 min readApr 20, 2021

We often hear that concept of Variance is hard to understand. Even most of the books and tutorials don’t put much emphasis on the topic.
But we are unknowingly using power of variance in very basic use of language features, daily. Also once you get the idea, it is very simple.

Complex data types, whats inside the types and how it is related

Be contravariant in the input type and covariant in the output type
- Robustness Principle

Lets start from the basics, Polymorphism

Polymorphism has below two forms
1. Subtyping
2. Generics

Subtyping

All developers know that in SOLID principles, L stands for Liskov’s substitution principle. It simply means that a subtype can be substituted for their super type without failing the program. Let’s take an example of something that we all can relate in today’s life.

trait CellPhone {
val brand: String
def makeCall: String
}

class BasicPhone(val brand:String) extends CellPhone {
override def makeCall: String = "calling from 2G network."
}

class FeaturePhone(val brand:String) extends CellPhone {
override def makeCall: String = "calling from 3G network."
}

class QwertyPhone(brand:String) extends FeaturePhone(brand)

class SmartPhone(val brand:String) extends CellPhone {
override def makeCall: String = "calling from 4G network."
}

As making a phone call is the most basic functionality every cell phone should offer, consider following method which takes a cell phone and make call from it. We can pass in any subtype of CellPhone where expected type is CellPhone and it should work.

def makeCallFrom(cellPhone: CellPhone) = cellPhone.makeCallmakeCallFrom(new FeaturePhone("Nokia")) 
makeCallFrom(new SmartPhone("Samsung"))

Pretty simple, isn’t It ?

Generics

Using generics we can make classes, functions work in many forms by defining them generic on the data they operate on. Let’s understand with example. We need to find out nth element from a list.

def getNthElem[T](list: List[T], index: Int):T = ..some logical code

In above method we don’t really bother about the element type of list. By defining the method generic on its data type we can use it with List of String, Int etc etc i.e. in many forms.

Lets define a generic class to work in many forms

trait Box[T]{
def extractItem : T
}

val featurePhoneInside = new Box[FeaturePhone] {
override def extractItem = FeaturePhone("Nokia")
}

val smartPhoneInside = new Box[SmartPhone] {
override def extractItem = SmartPhone("Apple")
}

we can use box in many forms now with specifying the value for type parameter T of Box[T].

Complex Data types
Complex data types are, types which holds another data type as component.
The component is specified by type parameter e.g. List[String]. List is a complex type, and String is component type for List. Same applies for Box[T] as well.

Now we know subtyping and complex data types.

In our example above Box[SmartPhone] is complex data type, and its component type is SmartPhone.

Variance
Variance is subtyping relationship of complex data type, depending upon the subtyping relationship of its component data type.

Confused ?

Let’s understand it through our earlier example, variance is set of rules to define subtyping relationship of Box, depending upon the subtyping relationship of SmartPhone e.g. you can say Box[QwertyPhone] is a subtype of Box[FeaturePhone]. Variance has 3 different modes depending on specific rules

i. Invariance
ii. Covariance
iii. Contravariance

Let’s understand above modes with the help of our CellPhone example.
QwertyPhone are feature phones with qwerty key pads.

QwertyPhone → subtype Of → FeaturePhone and

FeaturePhone → subtype Of → CellPhone.

Covariance (denoted by ‘+’)
Box[QwertyPhone] is a subtype of Box[FeaturePhone] which is subtype of Box[CellPhone]. Subtyping relationship of component type is applied to complex type.

Contravariance (denoted by ‘-’)
Reverse of covariance is Contravariance i.e.
Box[CellPhone] is a subtype of Box[FeaturePhone] which is subtype of Box[QwertyPhone]

Invariance
No matter what subtyping relationship the component type has, it does not applies to complex data type is Invariance. i.e. Box[CellPhone], Box[FeaturePhone], Box[QwertyPhone] are not related to each other in any way.
Liskov’s substitution principle does not apply in case of Invariance which means we cannot substitute Box[FeaturePhone]where Box[CellPhone]is expected.

Important question is, when are these modes of Variance needed in our day to day programming. If we create domain specific types in our code base (which is the recommended practice) then we need it for sure.

Let’s design domain objects for an e-commerce backend system. This Platform is dealing with all the Cell phone stores in given region. To start with, the users can only see the catalogue online and purchase the CellPhone
by visiting the store. we can define Catalogue as

Covariant data type
Catalogue always returns the list of items its holding.

// denoted by + on component type
trait Catalogue[+T] {
def itemList: List[T]
}

case class CellPhoneCatalogue() extends Catalogue[CellPhone] {
override def itemList: List[CellPhone] =
List(new BasicPhone("Nokia"), new SmartPhone("Samsung"))
}

case class SmartPhoneCatalogue() extends Catalogue[SmartPhone] {
override def itemList: List[SmartPhone] =
List(new SmartPhone("Samsung"))
}

We can see the Catalogue of SmartPhone is also Catalogue of CellPhone. If user is interested in CellPhones, he/she can be shown all the types of phones like Basic, Feature, Smart phones. We can iterate over all the stores of the region and present the user all types of cell phones that are available. We can define Catalogue as Covariant data type.
We can say that when we are returning generic type it can be covariant.

Most widely used structure in Scala day to day programming, immutable List is Covariant.

Contravariant data type
The domain experts of the system wants to onboard technicians who can repair the CellPhones. We need to be careful when designing data types like technicians because they are expert in some behaviour. This is possible that a technician may only know how to repair feature phone. Technician of smart phone may not know how to repair feature phone. More general technician like technician of cell phones knows how to repair all types of cell phones.

Hence we cannot pass technician of feature phone to repair cell phones as it is out of his/her expertise, and program will fail at run time. But we can pass more general technician, technician of CellPhones who knows all types of cell phones. We can define Technician as Contravariant data type.

trait Technician[-T] {
val name: String
def repair(product: T): Bill
}

which means we can substitute Technician[CellPhone] where Technician[SmartPhone] is expected. This is reverse of the Subtyping relationship of CellPhone.
We can say when we are accepting the generic data type it should be Contravariant.

We use functions everywhere in scala. Functions are contravariant in their argument type.

Invariant data type
Now as we know there are technicians available in the region, Store owners have decided to on board technicians to provide maintenance services on floor to better the user experience. Now Store has technician of particular expertise and a method service which returns the serviced phones. We are not interested in subtyping relationship of Store. We can have General Stores with all type of Phones available. Specific stores say for Smartphones. Our Store looks like

trait Store[T] {
def catalogue : Catalogue[T]
val technician: Technician[T]
def service(product: T): T
}

Store[SmartPhone] is not subtype of Store[CellPhone]. Neither reverse is true. User can visit specific stores to buy or service their phones.
When we are receiving and also returning the generic component then the type should be Invariant.

Conclusion
We have given another try to understand Variance starting right from polymorphism. When we create domain specific types variance is useful to make compiler work for you.
Also we have seen that there are few rules that we can abstract from the use of variance
1. Use covariance when returning the component type.
2. Use contravariance when accepting the component type
3. Use invariance when we doing both accepting and returning component type.

In scala, above rules are taken care by the language itself when defining custom complex data types.
You might have seen this error “covariant type T occurs in contravariant position” if you violate above rule no 2(topic for another blog may be).

Arshadali Syed is an experienced, hands-on software consultant at KodeYoga, he is involved in designing and implementing distributed systems and high performance cloud solutions with functional programming. You can connect with him over Twitter or Linkedin. Feel free to connect with us on hello@kodeyoga.com.

--

--