Scala Variances: Covariance, Contravariance, and Invariance

scala variances
Reading Time: 4 minutes

1. Variance

Variance is the interconnection of subtyping relationship between complex types and their component types.

Variance is all about sub-typing. It tells us if a type constructor is a subtype of another type constructor. Variance defines inheritance relationships of parameterized types(types that have parameters within them).

Sub-Typing

Every programming language supports the concept of types. Types give information about how to handle values at runtime. Subtyping adds more constraints to the values of a type.

Let’s see an example of subtyping:

sealed trait Polygon
case object Parallelogram extends Polygon
case object Rectangle extends Parallelogram
case object Square extends Rectangle

The type Square is a subtype of Rectangle, which is a subtype of Parallelogram, which is a subtype of trait Polygon.

Type Constructor and Parametrized Types

Scala supports generic types or type constructors to reuse code for many types at once. Type constructors are a mechanism that provides type variables that we can bind to concrete types.

Here T is known as Type Parameter and List[T] is known as Generic or Type Constructor.

For List[T], if we use List[Int], List[AnyVal], etc. then these List[Int] and List[AnyVal] are known as Parameterized Types.

2. Types of Variance

Scala supports three types of variance:

  • Covariance (Preserved)
  • Contravariance (Reversed)
  • Invariance (Ignored)

Scala supports variance annotations of type parameters of generic classes, to allow them to be covariant, contravariant, or invariant(if no annotations are used).

2.1. Covariance

If S is subtype of T then List[S] is a subtype of List[T].

This kind of inheritance relationship between two parameterized types is known as Covariance.

We declare a covariant type constructor using the following notation:

A type parameter T of a generic class can be made covariant by using the annotation +T.

Let’s see an example of a covariant type constructor:

class Shape[+T](polygon: T)

sealed trait Polygon
case object Parallelogram extends Polygon

We defined the type constructor Shape as covariant, which means that the type Shape[Parallelogram] is a subtype of Shape[Polygon]. The covariance property allows us to declare a variable like:

val shape: Shape[Polygon] = new Shape[Parallelogram](List(new Parallelogram))

Every time we need to assign a variable of type Shape[T], we can use an object of type Shape[R], given that Ris a subtype of T.

Covariance is type-safe because it reflects the standard behavior of subtyping. Assigning an object to a variable of one of its super-types is always safe.

In the above example, if we remove the covariant annotation from the type constructor Shape[T], the compiler warns us that we cannot use an object of type Parallelogram.

2.2. Contravariance

If S is subtype of T then List[T] is a subtype of List[S].

This relation is contrary to the covariance relation.

This kind of inheritance relationship between two parameterized types is known as Contravariance.

We declare a contravariant type constructor using the following notation:

A type parameter T of a generic class can be made contravariant by using the annotation -T.

Let’s see an example of a contravariant type constructor:

class Shape[+T](polygon: T)

case object Rectangle
case object Square extends Rectangle

We defined the type constructor Shape as contravariant, which means that the type Shape[Rectangle] is a subtype of Shape[Square]. The contravariance property allows us to declare a variable like:

val shape: Shape[Square] = new Shape[Rectangle](List(new Rectangle))

Every time we need to assign a variable of type Shape[T], we can use an object of type Shape[R], given that T is a subtype of R.

2.3. Invariance

If S is subtype of T then List[S] and List[T] don’t have inheritance relationship or sub-typing. That means both are unrelated.

Generic classes in Scala are invariant by default. This means that they are neither covariant nor contravariant.

We say that a type constructor F[_] is invariant if any subtype relationship between types A and B is not preserved in any order between types F[A]and F[B].

This kind of relationship between two parameterized types is known as Invariance or Non-Variance.

Let’s see an example of an invariant type constructor:

class Shape[T](polygon: T)

case object Parallelogram
case object Rectangle extends Parallelogram

The Shape[Parallelogram] accepts only Parallelogram type. Any super type or sub type is not accepted.

val suite: Shape[Parallelogram] = new Shape[Parallelogram](List(new Parallelogram))

3. Variance and Liskov Substitution Principle

Variance is related to the Liskov Substitution Principle (LSP) telling that “functions that use pointers to base classes must be able to use objects of derived classes without knowing it”.

This means that if S is a subtype of T, then the objects of type T maybe replaced with objects of type S without changing the behavior of T.

4. Conclusion

We looked at the three types of variance: covariance, contravariance, and invariance.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading