1. Overview

In this tutorial, we will be looking at Scala variances. Variance tells us if a type constructor (equivalent to a generic type in Java) is a subtype of another type constructor.

We’ll also take a look at the three main types of variance – invariance, covariance, and contravariance – and how they differ.

2. Subtyping and Type Constructors

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

For simple types, the story is straightforward:

sealed trait Test 
class UnitTest extends Test 
class IntegrationTest extends UnitTest 
class FunctionalTest extends IntegrationTest

The type FunctionalTest is a subtype of IntegrationTest, which is a subtype of the class UnitTest.

Additionally, many programming languages also support generic types or type constructors. Type constructors are a mechanism that creates new types starting from old ones. They provide type variables that we can bind to concrete types.

Let’s say that we want to model the result of a test on an object of type generic type T. We can use a type constructor to model such a situation:

class TestResult[T](id: String, target: T) {
  // Class behavior
}

Variance defines the subtyping relationship among type constructors, using the subtyping relationship among the types that bind their type variables. In other words, for a type constructor F[_], if B is a subtype of A, variance describes the relationship between the type F[B] and the type F[A].

3. Variances

There are three types of variance: covariance, contravariance, and invariance. Let’s look at each of them in detail.

3.1. Covariance

Covariance is a concept that is very straightforward to understand. We say that a type constructor F[_] is covariant if B is a subtype of type A and F[B] is a subtype of type F[A]. In Scala, we declare a covariant type constructor using the notation F[+T], adding a plus sign on the left of the type variable.

Consider our Test type hierarchy from the previous section. We are assuming the type UnitTest is at the base of the hierarchy, and that at each step, we are adding more sophisticated and evolved features. Often, we need to execute tests in a suite, and not only in isolation. A test suite is nothing more than a list of tests:

class TestsSuite[+T](tests: List[T])

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

val suite: TestsSuite[Test] = new TestsSuite[UnitTest](List(new UnitTest))

Every time we need to assign a variable of type TestsSuite[T], we can use an object of type TestsSuite[R], given that R is a subtype of T. In this case, covariance is type-safe because it reflects the standard behavior of subtyping. Assigning an object to a variable of one of its supertypes is always safe.

If we remove the covariant annotation from the type constructor TestsSuite[T], the compiler warns us that we cannot use an object of type UnitTest in the above example:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.UnitTest]
 required: com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.Test]
Note: com.baeldung.scala.variance.Variance.UnitTest <: com.baeldung.scala.variance.Variance.Test, but class TestsSuits is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
  val suite: TestsSuits[Test] = new TestsSuits[UnitTest](List(unitTest))
                                ^

There are many examples in the Scala SDK of type constructors declared as covariant concerning their type variable. The more popular are List[T], Option[T], and Try[T], to name a few.

3.2. Contravariance

We say that a type constructor F[_] is contravariant if B is a subtype of type A and F[A] is a subtype of type F[B]. This relation is precisely the contrary of the covariance relation. In Scala, we declare a contravariant type constructor using the notation F[-T], adding a minus sign on the left of the type variable.

At first sight, contravariance may seem counterintuitive. Why do we need to have this type of relationship for type constructors? Let’s define a hierarchy of classes, modeling an employee’s domain model:

class Person(val name: String)
class Employee(name: String, val salary: Int) extends Person(name)
class Manager(name: String, salary: Int, val manages: List[Employee]) extends Employee(name, salary)

We can define a type constructor that represents a test assertion:

class Assert[-T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

An instance of type Assert is a function from a generic type T to Boolean. It should verify that some property holds an object of type T, called target.

A list of Assert on the employee hierarchy verifies conditions on the attributes of the classes:

val personAssert = new Assert[Person](p => p.name == "Alice")
val employeeAssert = new Assert[Employee](e => e.name == "Bob" && e.salary) 
val managerAssert = new Assert[Manager](m => m.manages.nonEmpty)

It is feasible that we want to test more than one Assert on the same target object. We can define an aggregate type Asserts:

trait Asserts[T] {
  def asserts: List[Assert[T]]
  def execute(target: T): Boolean =
    asserts
      .map(a => a.assert(target))
      .reduce(_ && _)
}

The type constructor Asserts is a list of Assert that we want to execute on the same target object. We can specialize Asserts on the Employee type, obtaining a list of Assert that we can run on an Employee instance:

class AssertsEmployee(val asserts: List[Assert[Employee]]) extends Asserts[Employee]

What kind Assert can we test on an Employee? The type constructor Assert is defined as a contravariant, which means that Assert[Person] is indeed a subtype of Assert[Employee]. Therefore, the list of Assert can contain instances of either Assert[Employee] or Assert[Person]:

val bob = new Employee("Bob", 50000) 
val tester = new AssertsEmployee(List(personAssert, employeeAssert)) 
tester.execute(bob)

We can execute an Assert[Empoyee] on an object of type Employee, testing the attribute name and the attribute salary. We can also run an Assert[Person] on an object of type Employee. In this case, an assert can test only the property name that an Employee owns.

If we try to remove the contravariance annotation from the definition of the Assert type constructor, the compiler warns us that we are missing something:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
Note: com.baeldung.scala.variance.Variance.Person >: com.baeldung.scala.variance.Variance.Employee, but class Assert is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
  val tester = new AssertsEmployee(List(personAssert, employeeAssert))
                                        ^

What about the type Assert[Manager]? An assert on a manager could test the attribute manages that an object of type Employee does not have:

val tester = new AssertsEmployee(List(managerAssert))
tester.execute(bob)

Therefore, the compiler warns us that we cannot use an object of type Assert[Manager]:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Manager]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
  val tester = new AssertsEmployee(List(managerAssert))
                                      ^

In the Scala SDK, the most popular contravariant type constructor is Function1[-T1, +R]. The type constructor represents a function with one parameter of type T1. As we have already seen, when we use a type variable as an input parameter to a function or a method, contravariance comes to the rescue, allowing us to define type-safe code.

3.3. Invariance

The third type of relationship between a type constructor and its type variables is invariance. 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].

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

class Assert[T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

Let’s try to assign a variable of type Assert[Person] to an object of type Assert[Employee]:

val personAssert: Assert[Person] = new Assert[Employee](p => p.name == "Alice")

Due to the invariance on T, the compiler warns us correctly:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
Note: com.baeldung.scala.variance.Variance.Employee <: com.baeldung.scala.variance.Variance.Person, but class Assert is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
  val personAssert: Assert[Person] = new Assert[Employee](p => p.name == "Alice")
                                     ^

Invariance is the only type of relationship available in many programming languages that allows the use of type constructors, similar to Java and C++.

4. Conclusion

In this article, we introduced the concept of type constructors or generic types. Starting from the notion of subtyping defined for simple types, we showed how the subtype concept translates to type constructors through the definition of variance.

We looked at the three types of variance: covariance, contravariance, and invariance. We concluded that if we use a generic type variable assignment, we need to use covariance, and if we use a generic type as an argument, we need to use contravariance.

As always, the code from this article can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments