Scala - Variances



Variance tells us if one type constructor is a subtype of another. Variance is how subtyping relationships works with either constituent types or complicated types. Variance explains how types with parameters or arguments inherit from each other. These types are part of generic classes which take a type as a parameter. You can create relationships between complicated types, without it, you will not be able to reiterate abstraction class.

Subtyping and Type Constructors

All programming languages have the concept of types. Types guide a program on how to handle values during runtime. Subtyping introduces extra constraints on type values.

Simple types have a straightforward story. For example,

sealed trait Animal
class Mammal extends Animal
class Cat extends Mammal
class Lion extends Cat

The type Lion is a subtype of Cal which is a subtype of the class Mammal.

Many languages also have generic types or type constructors. Type constructors create new types from existing ones. They provide type variables to bind to specific types.

Suppose we want to represent the test result on an object of generic type R. We can use a type constructor to represent this scenario. For example,

class ResultData[R](code: Int, data: R) {
  // Class behavior
}

Variance defines subtyping relationships among type constructors. It is done based on the relationships among the types they bind. In other words, for type constructor F[_]. If B is a subtype of A. Then variance describes the relationship between F[B] and F[A].

Types of Scala Variance

The Scala Variances are of three types, which are as follows:

  • Covariant
  • Contravariant
  • Invariant

These are explained as follows below.

1. Covariance

Covariance is a straightforward concept to understand. Type constructor F[_] is covariant if B is a subtype of A and F[B] is a subtype of F[A]. In Scala, a covariant type constructor is declared as F[+R]. With a plus sign on the side of the type variable.

Consider our Animal type hierarchy from the previous example. We are assuming the type Mammal is the base of the hierarchy, and that at each step, we are including more features. We need to run class Animal in a suite but not in isolation. Animal suite is nothing more than a list of Animals:

class AnimalsCollection[+A](animals: List[A])

We have defined the type constructor AnimalsCollection as covariant, which means that the type AnimalsCollection[Cat] is a subtype of Cat[Mammal]. The covariance property allow us to declare a variable like:

val zoo: AnimalsCollection[Animal] = new AnimalsCollection[Mammal](List(new Lion, new Cat))

Every time we need to assign a variable of type AnimalsCollection[R], we can use it as an object of type AnimalsCollection[T], given that T is a subtype of R. In this scenario, covariance is type-safe as it reflects standard subtyping behavior. Assigning an object to a variable of its supertype is always safe. If we remove the covariant annotation from AnimalsCollection[R], the compiler warns against using a AnimalsCollection object in the example above.

type mismatch;
 found   : AnimalsCollection[Mammal]
 required: AnimalsCollection[Animal]
Note: Mammal <: Animal, but class AnimalsCollection is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
val zoo: AnimalsCollection[Animal] = new AnimalsCollection[Mammal](List(new Lion, new Cat))

Scala SDK has many examples of covariant type constructors, like List[R], Option[R], and Try[R].

2. Contravariance

A type constructor F[_] is contravariant if B is a subtype of A and F[A] is a subtype of F[B]. This relation is the opposite of the covariance relation. In Scala, a contravariant type constructor is declared as F[-T]. With a minus sign on the left of the type variable.

At first view, contravariance may seem counterintuitive to you. Why do we need this kind of relationship for type constructors? Let's create a class hierarchy to model an Course domain:

class Department(val name: String)
class Course(name: String, val creditHours: Int) extends Department(name)
class Student(name: String, val enrolledCourses: List[Course]) extends Course(name, 0)

We can create a type constructor representing a validator assertion. For example,

class Validator[-T](validateFunction: T => Boolean) {
  def validate(input: T): Boolean = validateFunction(input)
}

Validator instances are functions saying if a T type is true or false. It needs to check if a property is true for an object of type T which is known as the target.

A list of Assert for the course hierarchy checks conditions on class attributes:

val departmentValidator = new Validator[Department](d => d.name.nonEmpty)
val courseValidator = new Validator[Course](c => c.name.nonEmpty && c.creditHours >= 0)
val studentValidator = new Validator[Student](s => s.name.nonEmpty && s.enrolledCourses.nonEmpty)

We might want to test multiple Validators on the same target object. We can create a type called Validators for grouping multiple Validators. For example,

trait Validators[T] {
  def validators: List[Validator[T]]
  def validate(target: T): Boolean =
    validators
      .map(v => v.validate(target))
      .reduce(_ && _)
}

The Asserts type constructor is a list of Validators meant to run on one target. We can specialize Validators on the Course type, obtaining a list of Validators that we can run on a Student instance. For example,

class ValidatorsDepartment(val validators: List[Validator[Department]]) extends Validators[Department]
class ValidatorsCourse(val validators: List[Validator[Course]]) extends Validators[Course]
class ValidatorsStudent(val validators: List[Validator[Student]]) extends Validators[Student]

What kind of Validator can we test on a Student? The type constructor Validator is defined as a contravariant, which means that Validator[Person] is indeed a subtype of Validator[Student]. Therefore, the list of Validators can contain instances of either Validator[Student] or Validator[Person]. For example,

val mathDepartment = new Department("Mathematics")
val calculus = new Course("Calculus", 4)
val alice = new Student("Alice", List(calculus))

val departmentValidators = new ValidatorsDepartment(List(/* Add Department validators here */))
val courseValidators = new ValidatorsCourse(List(/* Add Course validators here */))
val studentValidators = new ValidatorsStudent(List(/* Add Student validators here */))

val departmentValid = departmentValidators.validate(mathDepartment)
val courseValid = courseValidators.validate(calculus)
val studentValid = studentValidators.validate(alice)

println(s"Is the Department valid? $departmentValid")
println(s"Is the Course valid? $courseValid")
println(s"Is the Student valid? $studentValid")

We can execute a validator [Student] on an object of type Student. Testing its attribute names and attribute list values. We can also run a validator [Person] on an object of type Student. In this case, a validator can only test the property name that a student owns.

If we remove the contravariance annotation from the Assert type constructor. The compiler warns that something is missing. For example,

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Validator[com.baeldung.scala.variance.Variance.Person]
 required: com.baeldung.scala.variance.Variance.Validator[com.baeldung.scala.variance.Variance.Student]
Note: com.baeldung.scala.variance.Variance.Person >: com.baeldung.scala.variance.Variance.Employee, but class Validator is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
val departmentValidators = new ValidatorsDepartment(List(personValidator, studentValidator))
                                                                      ^

In Scala SDK, the widely used contravariant type constructor is Function1[-T1, +R]. This type constructor is like a function that takes one input of type T1.

3. Invariance

The third relationship between a type constructor and its variables is invariance. Type constructor F[_] is invariant if it does not preserve subtype relationships between F[A] and F[B] for any order of types A and B.

If we remove the contravariance property (-) from the previous Assert type, we get an invariant type constructor:

class Validator[T](validateFunction: T => Boolean) {
  def validate(target: T): Boolean = validateFunction(target)
}

Lets try to assign a variable of type Validator[Animal] to an object of type Validator[Mammal]:

val animalValidator: Validator[Animal] = new Validator[Mammal](a => a.sound == "Roar")

The compiler will warn us because of the invariance:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Validator[com.baeldung.scala.variance.Variance.Mammal]
 required: com.baeldung.scala.variance.Variance.Validator[com.baeldung.scala.variance.Variance.Animal]
Note: com.baeldung.scala.variance.Variance.Mammal <: com.baeldung.scala.variance.Variance.Animal, but class Validator is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
val animalValidator: Validator[Animal] = new Validator[Mammal](a => a.sound == "Roar")

In many programming languages like Java and C++, invariance is the only relationship available for using type constructors.

Advertisements