Variance is the quality of being different. It is the correlation of subtyping relationships of complex types and subtyping relationships of their component types.
Covariance allows assigning an instance to a variable whose type is one of the instance’s generic type; i.e. supertype.
Contravariance allows assigning an instance to a variable whose type is one of the instance’s derived type; i.e. subtype.
Before learning about variances, prerequisite is to first understand : Type System and Type Parameterization.
Liskov substitution principle
Liskov substitution principle (the L. of SOLID principles) specifies that, in a relation of inheritance, a type defined as supertype allows to substitute it by any of its derived classes.
Let’s see how this happens
scala> abstract class Vehicle defined class Vehicle scala> case class Car() extends Vehicle defined class Car scala> case class Bike() extends Vehicle defined class Bike
Now let’s define a function which takes a Vehicle as a parameter and returns the same vehicle value.
scala> val vehicleIdentity = (vehicle:Vehicle) => vehicle vehicleIdentity: Vehicle => Vehicle = <function1>
Now, we can invoke the vehicle Identity function as
scala> vehicleIdentity(Car()) res0: Vehicle = Car() scala> vehicleIdentity(Bike()) res1: Vehicle = Bike()
So here, we can substitute Car and Bike in place of Vehicle, since Car and Bike are subclass of Vehicle.
Variance annotation creates a type hierarchy between parameterized types. In other words, given a class List [A],
if A is a subclass of B then, List [A] can be a subclass of List [B].
The variance models this correlation and allows us to create more reusable generic classes.
Let’s see how variance works.
scala> case class Parking[A](value: A) defined class Parking scala> val carParking: Parking[Vehicle] = Parking[Car](new Car) <console>:12: error: type mismatch; found : Parking[Car] required: Parking[Vehicle] Note: Car <: Vehicle, but class Parking is invariant in type A.
Here, Car is a subtype of Vehicle but Parking[Car] isn’t subtype of Parking[Vehicle]. Hence we aren’t able to assign the Parking[Car] in place of Parking[Vehicle].
Before moving to details of covariance and contravariance, below image is a good example to start with.
A generic class covariant over its abstract type can receive a parameter type of that type or subtypes of that type.
scala> abstract class Vehicle defined class Vehicle scala> case class Car() extends Vehicle defined class Car scala> case class Parking[+A](vehicle: A) defined class Parking scala> val carParking : Parking[Vehicle] = Parking[Car](new Car) carParking: Parking[Vehicle] = Parking(Car())
Legal positions of covariant type parameter
The covariant type parameter can be used as:
immutable field type,
method return type,
method argument type( if the method argument type has a lower bound )
Because of these restrictions, covariance is most commonly used in producers (types that return something) and immutable types.
To understand contravariance, stop thinking of types in terms of “is a more specialized type” and switch the focus to the idea of acceptance. Contravariant is the way to express that a Container can be either the basic type or only specialized for a given type?
scala> case class Parking[-A]() defined class Parking scala> val parking: Parking[Car] = Parking[Vehicle] parking: Parking[Car] = Parking()
Use cases for contravariant type parameter
Contravariant type parameter is usually used as a method argument type. Contravariance is most commonly associated with consumers (types that accept something).
Use restrictions of covariant type parameter
Contravariant type parameter would be illegal in a position such as a method return type then Scala compiler would have reported an error.
In general, parameterized types that are covariant in the type parameter are producers of that type parameter and those that are contravariant in the type parameter are consumers of the type parameter.