Kotlin Generic Variance Modifiers
Let's say that Puppy
is a subtype of Dog
, and you have a generic Box
class to enclose them both. The question is: what is the relation between the Box<Puppy>
and Box<Dog>
types? In other words, can we use Box<Puppy>
where Box<Dog>
is expected, or vice versa? To answer these questions, we need to know what the variance modifier of this class type parameter is1.
When a type parameter has no variance modifier (no out
or in
modifier), we say it is invariant and thus expects an exact type. So, if we have class Box<T>
, then there is no relation between Box<Puppy>
and Box<Dog>
.
Variance modifiers determine what the relationship should be between Box<Puppy>
and Box<Dog>
. When we use the out
modifier, we make a covariant type parameter. When A
is a subtype of B
, the Box
type parameter is covariant (out
modifier) and the Box<A>
type is a subtype of Box<B>
. So, in our example, for class Box<out T>
, the Box<Puppy>
type is a subtype of Box<Dog>
.
When we use the in
modifier, we make a contravariant type parameter. When A
is a subtype of B
and the Box
type parameter is contravariant (in
modifier), then type Box<B>
is a subtype of Box<A>
. So, in our example, for class Box<in T>
the Box<Dog>
type is a subtype of Box<Puppy>
.
These variance modifiers are illustrated in the diagram below:
At this point, you might be wondering how these variance modifiers are useful. In particular, contravariance might sound strange to you, so let me show you some examples.
List variance
Let's consider that you have the type Animal
and its subclass Cat
. You also have the standalone function petAnimals
, which you use to pet all your animals when you get back home. You also have a list of cats that is of type List<Cat>
. The question is: can you use your list of cats as an argument to the function petAnimals
, which expects a list of animals?
The answer is YES. Why? Because in Kotlin, the List
interface type parameter is covariant, so it has the out
modifier, which is why List<Cat>
can be used where List<Animal>
is expected.
Covariance (out
) is a proper variance modifier because List
is read-only. Covariance can’t be used for a mutable data structure. The MutableList
interface has an invariant type parameter, so it has no variance modifier.
Thus, MutableList<Cat>
cannot be used where MutableList<Animal>
is expected. There are good reasons for this which we will explore when we discuss the safety of variance modifiers. For now, I will just show you an example of what might go wrong if MutableList
were covariant: we could use MutableList<Cat>
where MutableList<Animal>
is expected and then use this reference to add Dog
to our list of cats. Someone would be really surprised to find a dog in a list of cats.
This illustrates why covariance, as its name out
suggests, is appropriate for types that are only exposed and only go out of an object but never go in. So, covariance should be used for immutable classes.
Consumer variance
Let's say that you have a class that can be used to send messages of a certain type.
Now, you’ve made a class called GeneralSender
that is capable of sending any kind of message. The question is: can you use GeneralSender
where a class for sending some specific kind of messages is expected? You should be able to! If GeneralSender
can send all kinds of messages, it should be able to send specific message types as well.
For a sender of any message to be a sender of some specific message type, we need the sender type to have a contravariant parameter, therefore it needs the in
modifier.
Let's generalize this and consider a class that consumes objects of a certain type. If a class declares that it consumes objects of type Number
, we can assume it can consume objects of type Int
or Float
. If a class consumes anything, it should consume strings or chars, therefore its type parameter, which represents the type this class consumes, must be contravariant, so use the in
modifier.
It makes a lot of sense to use contravariance for the consumer or sender values as both their type parameters are only used in the in-position as argument types, so covariant type values are only consumed. I hope you’re starting to see that the out
modifier is only appropriate for type parameters that are in the out-position and are thus used as a result type or a read-only property type. On the other hand, the in
modifier is only appropriate for type parameters that are in the in-position and are thus used as parameter types.
Function types
In function types, there are relations between function types with different parameters and result types. To see this in practice, think of a function that as an argument expects a function that accepts an Int
and returns an Any
:
Based on its definition, such a function can accept a function of type (Int)->Any
, but it would work with (Int)->Number
, (Number)->Any
, (Number)->Number
, (Any)->Number
, (Number)->Int
, etc.
This is because there is the following relation between all these types:
Notice that when we go down in this hierarchy, the parameter type moves toward types that are higher in the typing system hierarchy, and the return type moves toward lower types.
This is no coincidence. All parameter types in Kotlin function types are contravariant, as the name of the in
variance modifier suggests. All return types in Kotlin function types are covariant, as the name of the out
variance modifier suggests.
In this case – as in many other cases – you don’t need to understand variance modifiers to benefit from using them. You just use the function you would like to use, and it works. People rarely notice that this would not work in another language or with another implementation. This makes a good developer experience. People don’t attribute this good experience to generic type modifiers, but they feel that using Kotlin or some libraries is just easier. As library creators, we use type modifiers to make a good developer experience.
The general rule for using variance modifiers is really simple: type parameters that are only used for public out-positions (function results and read-only property types) should be covariant so they have an out
modifier. Type parameters that are only used for public in-positions (function parameter types) should be contravariant so they have an in
modifier.
This is the end of the first part. In the next part, you will learn about an important pattern called the Covariant Nothing Object. Stay tuned!
In this chapter, I assume that you know what a type is and understand the basics of generic classes and functions. As a reminder, the type parameter is a placeholder for a type, e.g., T
in class Box<T>
or fun a<T>() {}
. The type argument is the actual type used when a class is created or a function is called, e.g., Int
in Box<Int>()
or a<Int>()
. A type is not the same as a class. For a class User
, there are at least two types: User
and User?
. For a generic class, there are many types, like Box<Int>
, and Box<String>
.