DEV Community

Andrew (he/him)
Andrew (he/him)

Posted on

Understand Type Variances in 30 Seconds

Type variances are a topic which most Java developers don't usually think about, but which can take some getting used to for those transitioning into languages which do make heavy use of them, like Scala.

There are three possible kinds of type variance

  1. invariance
  2. covariance
  3. contravariance

Recall that, in Scala, the notation T <: S means that T extends or "subclasses" S, while T >: S means the converse -- that S extends or subclasses T.

You may notice that T >: S means the same as S <: T, so why are there two different ways to say this? Well, you might want to define both an upper (<:) and a lower (>:) bound for your type. For example, if you wrote a class hierarchy that mirrored taxonomic ranks, you might want a type that is larger than Species, but less than Order, in which case you would write Genus <: T <: Family.

You can think of T <: S as meaning "T is a type which is less than or equal to type S" (in a subclassing / extending sense), while T >: S means that "T is greater than or equal to type S". Since there are no taxonomic ranks between Genus and Family, Genus <: T <: Family means that type T can be either Genus or Family.

Variance

It's important to remember that when we talk about variance, we're talking about the types themselves, but also the "container types" which are parameterized by them.

What this means is that you can't talk about type variance without some parameterized type like Set[A] or List[+A] or Map[K, +V]. We're interested in the parameterized types Set, List, and Map, as they relate to their type parameters.

Invariance

A parameterized type is invariant in its type parameter if there is no subclassing relationship when using different type parameters. For example, in Scala, Set[A] is invariant in its type parameter A. This means that

    A <: B  =>  Set[A] <: Set[B] is false
    A >: B  =>  Set[A] >: Set[B] is false
    A <: B  =>  Set[A] >: Set[B] is false
    A >: B  =>  Set[A] <: Set[B] is false
Enter fullscreen mode Exit fullscreen mode

In other words, no matter the relationship between A and B, you cannot substitute a Set[A] for a Set[B] and vice versa. A Set[String] is not a Set[Any], for example, and a Set[Numeric] is not a Set[Int].

If you're coming from a Java background, this is the variance you're probably most familiar with. Java was initially developed without generics (no type parameters!), so invariance was really the only solution.

Covariance

A parameterized type is covariant in its type parameter if there is a parallel subclassing relationship when using different type parameters. For example, in Scala, List[+A] is covariant in its type parameter +A. (That's what the + means before the generic type A.) This means that

    A <: B  =>  List[A] <: List[B] is true
    A >: B  =>  List[A] >: List[B] is true
    A <: B  =>  List[A] >: List[B] is false
    A >: B  =>  List[A] <: List[B] is false
Enter fullscreen mode Exit fullscreen mode

This is the more "intuitive" type variance, in my opinion. In this case, we might have a subclassing relationship like Dog <: Animal, because a Dog is a kind of Animal. In that case, a List[Dog] <: List[Animal] because a list of dogs is a kind of list of animals.

Wherever we require a List[Animal], we can provide a List[Dog], because List[Dog] <: List[Animal] (read as "List of Dog is a kind of List of Animal").

Contravariance

Contravariance is the "opposite" of covariance. A parameterized type is contravariant in its type parameter if there is an anti-parallel subclassing relationship when using different type parameters. For a parameterized type which is contravariant in its type parameter, the following relationships hold

    A <: B  =>  List[A] <: List[B] is false
    A >: B  =>  List[A] >: List[B] is false
    A <: B  =>  List[A] >: List[B] is true
    A >: B  =>  List[A] <: List[B] is true
Enter fullscreen mode Exit fullscreen mode

This one is a bit more difficult to explain.

Imagine we have a parameterized Teacher[-S] class, where S gives the Subject the Teacher is qualified to teach. The - indicates that Teacher is contravariant in its type parameter S.

As for the Subject, S, we might say that TypeVariance is a kind of programming concept

TypeVariance <: ProgrammingConcept
Enter fullscreen mode Exit fullscreen mode

...both of which are Subjects which our Teacher might teach.

If our Teacher is qualified to teach ProgrammingConcepts, she must also be qualified to teach TypeVariance (otherwise, she wouldn't be qualified to teach ProgrammingConcepts).

In other words, wherever we require a teacher who is qualified to teach TypeVariance, we can substitute a teacher who is qualified to teach ProgrammingConcepts in general

Teacher[ProgrammingConcept] <: Teacher[TypeVariance]
Enter fullscreen mode Exit fullscreen mode

Since TypeVariance is a kind of ProgrammingConcept, a Teacher of ProgrammingConcepts is a Teacher of TypeVariance.

Oldest comments (0)