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
- invariance
- covariance
- 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
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
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
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
...both of which are Subject
s which our Teacher
might teach.
If our Teacher
is qualified to teach ProgrammingConcept
s, she must also be qualified to teach TypeVariance
(otherwise, she wouldn't be qualified to teach ProgrammingConcept
s).
In other words, wherever we require a teacher who is qualified to teach TypeVariance
, we can substitute a teacher who is qualified to teach ProgrammingConcept
s in general
Teacher[ProgrammingConcept] <: Teacher[TypeVariance]
Since TypeVariance
is a kind of ProgrammingConcept
, a Teacher
of ProgrammingConcept
s is a Teacher
of TypeVariance
.