Scala Generics II: Covariance and Contravariance

Share This Post

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.

  ChatGPT: Use Cases in Software Development

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?

  Scala world insights at the Scala world conference

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>

 

  JSON in Kotlin: Comparing Options

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

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability

Author

  • Rafael Ruiz

    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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange