Scala supports using type parameters to implement a classes and functions that can support multiple types. Type parameters are very useful when creating a generics. However, advanced use cases may require you to specify a constraints on types used. That’s where a Variances help to model the relationships between generic types. This post will cover the topic of type variances in Scala.
What is Variance?
Variance is the correlation of subtyping relationships of complex types and the subtyping relationships of their component types.
Scala-lang.org
In other words, variance allows developers to model the relationships between types and component types. As a consequence, variance allows to create a clean and very usable generic classes that are widely used in various Scala libraries.
Scala supports thee types of variance: covariance, invariance and contravariance. With this in mind, let’s look at each of these in greater detail.
Covariance
Let’s assume that we have the following class structure
abstract class Vehicle {
def name: String
}
case class Car(name: String) extends Vehicle
case class Bus(name: String) extends Vehicle
class VehicleList[T](val vehicle: T)
The Car
and Bus
classes both inherit from abstract class Vehicle
. Considering that class Car has its own collection VehicleList[Car]
: is VehicleList[Car]
a subtype of VehicleList[Vehicle]
? The answer is no. Despite the fact that Car
class extends a Vehicle
class, the same can't be said about VehicleList[Car]
and VehicleList[Vehicle]
.
val vehicleList: VehicleList[Vehicle] = new VehicleList[Car](new Car("bmw")) // incorrect: type mismatch
The solution to this is to use a covariance. As a consequence, a VehicleList[Car]
will be a subtype of VehicleList[Vehicle]
. This allows for greater polymorphism with generic types. To make VehicleList
class T
parameter covariant, just add a little + sign along the type:
class VehicleList[+T](val vehicle: T)
val vehicleList: VehicleList[Vehicle] = new VehicleList[Car](new Car("bmw")) // correct
Contravariance
In contrast to covariance, contravariance allows the generic type T
to be T
or a supertype of T
. For instance, let's consider the example class, similar to one used above:
class CarList[T](val car: T)
The CarList
class expects to get type parameter T
and no other super or sub type. What if we want to use the CarList[Car]
class to store the Vehicle class instances as well?
val carList: CarList[Car] = new CarList[Vehicle](new Vehicle("boat")) // incorrect: type mismatch
To address that, instead of a + sign in the type definition, use the - sign.
class CarList[-T](val car: T)
val carList: CarList[Car] = new CarList[Vehicle](new Vehicle("boat")) // correct
Invariance
Invariance is the default relationship of generic type T
. The List[Car]
accepts only Car
type - any super type or sub type is not accepted.
class List[T](val t: T)
Summary
To summarize, Scala and variance system is a very handy tool to model relationships between generics types. I hope this post helped you to understand this better, so you can start using it right away!
Utilize Scala's type system to model advanced generic type relationships. Covariance, contravariance and invariance explained using easy to understand code examples.
bartoszgajda.com/2020/02/02/var…
#scala #programming #functionalprogramming #variance #development #jvm12:35 PM - 02 May 2020
Top comments (0)