Table of Contents
In the previous article we looked at Scala Type bounds, today we will continue with Scala generics and talk about Covariance and Contravariance in generics.
Liskov substitution principle (the L. of S.O.L.I.D.) specifies that, in a relation of inheritance, a type defined as supertype must be in a context that in any case, it allows to substitute it by any of its derived classes.
To illustrate it, we will use this set of classes
sealed abstract class Vehicle
case class Car() extends Vehicle
case class Motorcycle() extends Vehicle
At all times, given that we define:
val vehicleIdentity = (vehicle:Vehicle) => vehicle
It has to be correct to invoke:
vehicleIdentity(Car())
as well as:
vehicleIdentity(Motorcycle())
You may find more details here.
In the generic types, the variance is the correlation between the inheritance relation of the abstract types and how it is “transmitted” to the inheritance in the generic classes.
In other words, given a class Thing [A], if A inherits from B (A <: B), then Thing [A] <: Thing [B]?
The variance models this correlation and allows us to create more reusable generic classes.
There are four types of variance: covariance, contravariance, invariance and bivariance, three of which are expressed in Scala:
> Invariance: class Parking[A]
A generic class invariant over its abstract type can only receive a parameter type of exactly that type.
case class Parking[A](value: A) val p1: 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.
You may wish to define A as +A instead. (SLS 4.5)
val p1: Parking[Vehicle] = Parking[Car](new Car)
The error makes it clear, although Car <: Vehicle, due to that Parking is invariant over A, Parking [Car] !<: Parking [Vehicle].
However, once the abstract type is defined, it can be used freely within the class, applying the Liskov substitution principle:
val p1 = Parking[Vehicle](new Motorcycle)
res8: Parking[Vehicle] = Parking(Motorcycle())
p1.value.getClass
res9: Class[_ <: Vehicle] = class Motorcycle
> Covariance: class Parking[+A]
A generic class covariant over its abstract type can receive a parameter type of that type or subtypes of that type.
case class Parking[+A](value: A)
val p1: Parking[Vehicle] = Parking[Car](new Car)
p1: Parking[Vehicle] = Parking(Car())
The covariance allows you to type p1 as Parking[Vehicle] and assign it a Parking[Car].
But do not forget that although p1 is typed as Parking[Vehicle], it is actually a Parking[Car],
this can be confusing, but below I explain that they are the covariant and contravariant positions and you will eventually get it.
Summing up:
For Parking[+A]
If Car <: Vehicle
Then Parking[Car] <: Parking[Vehicle]
> Contravariance: class Parking[-A]
A generic class contravariant over its abstract type can receive a parameter type of that type or supertypes of that type.
case class Parking[-A]
val p1: Parking[Car] = Parking[Vehicle]
p1: Parking[Car] = Parking()
The contravariance allows us to type p1 as Parking [Car] and assign it a Parking [Vehicle]
Summing up:
For Parking[-A]
If Vehicle >:(supertype of) Car
Then Parking[Vehicle] <: Parking[Car]
Covariant and contravariant positions
A type can be in covariant or contravariant position depending on where it is specified. Some of good examples are the following:
class Pets[+A](val pet:A) {
def add(pet2: A): String = "done"
}
<console>:9: error: covariant type A occurs in contravariant position in type A of value pet2
What happened? We have typed the input parameter of the add () method as A (a covariant type because of Pets [+ A]).
The compiler has complained, it says that the add input parameter is in a contravariant position, it says that A is not valid. But why?
Because if I do:
val pets: Pets[Animal] = Pets[Cat](new Cat)
Although pets are typed as Pets [Animal], it is actually a Pets [Cat], therefore, because pets are typed as Pets [Animal], pets.add () will accept Animal or any subtype of Animal. But this does not make sense, since in fact pets is a Pets [Cat] and add () can only accept Cats or Cat subtype.
The compiler prevents us from falling into the absurdity of calling pets.add (Dog ()), since it is a set of Cat.
Another example, in the case of the contravariance is the next one:
class Pets[-A](val pet:A)
<console>:7: error: contravariant type A occurs in covariant position in type => A of value pet
class Pets[-A](val pet:A)
What happened? We have typed the input parameter as A (which is contravariant because Pets[-A])
and the compiler has told us that this parameter is in a covariant position, that we can not type it as A. But why?
Because if I do:
val pets: Pets[Cat] = Pets[Animal](new Animal)
The compiler would expect pets.pet to be Cat, an object able to do pets.pet.meow(), but pets.pet is not Cat, it is an Animal.
And although Pets[-A] is contravariant over A, the value pet: A it’s not, once its type is defined (pets val: Pets [Cat] implies that pets.pet will be Cat), this type is definitive.
If Pets were covariant over A (Pets [+ A]) this would not happen, because if we do:
val pets: Pets [Animal] = Pets [Cat] (new Cat)
the compiler would wait for pets.pet to be Animal, and because Cat <: Animal, it is.
Another example,
abstract class Pets[-A] {
def show(): A
}
<console>:8: error: contravariant type A occurs in covariant position in type ()A of method show
def show(): A
For the same reason as before, the compiler says that the return type of a method is in a covariant position, A is contravariant.
If I do:
val pets: Pets[Cat] = Pets[Animal](new Animal)
I would expect to be able to make pets.show.meow (), since pets it’s a Pets[Cat], show() will return a Cat.
But as we’ve discovered before, it’s actually a Pets[Animal] and show() will return an Animal.
Finally I would like to show the definition of Function1 (function that accepts one input parameter) in scala:
trait Function1[-T, +R] extends AnyRef {
def apply(v1: T): R
}
When creating a function, we are already informed that it is of type Function1:
If we have the classes: Class Animal, Class Dog extends Animal, Class Bulldog extends Dog and Class Cat extends Animal
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Animal
doSomething: Bulldog => Animal = <function1>
We have created a Function1 [Bulldog, Animal], but remember the variances that Function1 has, R is covariant, which means that although we declare it as Animal, we can return any subtype:
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Cat
doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Dog
doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Bulldog
doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Animal
doSomething: Bulldog => Animal = <function1>
This is because by Liskov we can substitute Animal for any of its subtypes.
But T is contravariant, therefore:
val doSomething:(Bulldog=>Animal) = (doggy: Dog) => new Animal
doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (animal: Animal) => new Animal
doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (kitty: Cat) => new Animal
/* <console>:11: error: type mismatch;
found : Cat => Animal
required: Bulldog => Animal
val doSomething:(Bulldog=>Animal) = (kitty: Cat) => new Animal
^ /*
In the position of T (Bulldog) we can type the input parameter as any Bulldog supertype,
since Bulldog will always comply with the inheritance (Bulldog is Dog, and is also an Animal). It’s Liskov again.
And this is all for now in terms of covariance, contravariance and invariance! In the upcoming article, Scala Generics III: The Type constraints we will immerse ourselves in the world of type constraints and we will finish the introduction to Scala generics!
If you found this article about covariance and contravariance in generics interesting, you might like…
Scala generics I: Scala type bounds
F-bound over a generic type in Scala
Microservices vs Monolithic architecture
Author
-
Software developer with over 8 years experience working with different code languages, able to work as a FullStack Developer, with the best skills in the backend side. Passionate about new technologies, and the best software development practices involved in a DevOps culture. Enjoying mentoring teams and junior developers and I feel very comfortable with the stakeholders management-wise.
View all posts