Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Scala Generics (Part 2): Covariance and Contravariance in Generics

DZone's Guide to

Scala Generics (Part 2): Covariance and Contravariance in Generics

Let's dive into Scala generics, the Liskov substitution principle, inheritance, covariance, contravariance, and invariance.

· Java Zone ·
Free Resource

Java-based (JDBC) data connectivity to SaaS, NoSQL, and Big Data. Download Now.

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.

The Liskov substitution principle (the L. of S.O.L.I.D.) specifies that, in a relation of inheritance, a type defined as a supertype must be in a context that, in any case, it allows you to substitute it with 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 val


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 generic types, the variance is the correlation between the inheritance relationship 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 is 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 that is 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, because 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())


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. 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 that is 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 a co- or contravariant position depending on where it is specified. Some 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?

Let's look at this example:

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 a 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 contravariance, is:

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 cannot type it as A. But why?

Because if I do:

val pets: Pets[Cat] = Pets[Animal](new Animal) 


Then 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 is not. Once its type is defined (pets val: Pets [Cat] implies that pets.pet will be Cat), this type is definitive.

If Pets was covariant over A (Pets [+ A]), this would not happen, because if we do:

val pets: Pets [Animal] = Pets [Cat] (new Cat)


Then 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 is 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 (a 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 class 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, through 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 (Part 3): Type Constraints, we will immerse ourselves in the world of type constraints and will finish the introduction to Scala generics!

Connect any Java based application to your SaaS data.  Over 100+ Java-based data source connectors.

Topics:
scala ,generics ,solid ,liskov substitution principle ,covariant return type ,contravariant ,java ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}