Kotlin Generic Variance Modifiers
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.
Let's say that Puppy
is a subtype of Dog
, and you have a generic Box
class to enclose them. The question is: what is the relation between Box<Puppy>
and Box<Dog>
types? In other words: can we use Box<Puppy>
, where Box<Dog>
is expected, or the other way around? To answer those questions, we need to know what is the variance modifier of this class type parameter1.
When a type parameter has no variance modifier (no out
or in
modifier), we say it is invariant, so it expects an exact type. So if we have class Box<T>
, then there is no relation between Box<Puppy>
and Box<Dog>
.
Variance modifiers decide what the relationship should be between Box<Puppy>
and Box<Dog>
. When we use the out
modifier, we make the type parameter covariant. When A
is a subtype of B
, the Box
type parameter is covariant (out
modifier), then type Box<A>
is a subtype of Box<B>
. So in our example, for class Box<out T>
, the type Box<Puppy>
is a subtype of Box<Dog>
.
When we use the in
modifier, we make the type parameter contravariant. 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 type Box<Dog>
is a subtype of Box<Puppy>
.
Those variance modifiers are illustrated in the below diagram:
At this point, you might be wondering how those variance modifiers are useful. Especially the contravariance might sound strange to you. 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? It is because in Kotlin, the List
interface type parameter is covariant, so it has the out
modifier. That is why List<Cat>
can be used where List<Animal>
is expected.
It is a proper variance modifier because List
is read-only. Covariance couldn't be used for a mutable data structure. The interface MutableList
has an invariant type parameter, so it has no variance modifier.
That is why MutableList<Cat>
cannot be used where MutableList<Animal>
is expected. There are good reasons behind that, and we will explore them when we discuss variance modifiers' safety. For now, I will just show you an example of what might go wrong if MutableList
would be covariant. We could then 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.
That illustrates why covariance, as its name out
suggests, is appropriate for types that are only exposed, that only go out, but never go in. So it 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 made a class called GeneralSender
, that is capable of sending any kind of messages. 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 that, we need a sender type with a contravariant parameter, so it needs the in
modifier.
Let's generalize it 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 of type Float
. If a class consumes anything, it should consume strings or chars. For that, its type parameter representing the type this class consumes must be contravariant, so use the in
modifier.
It makes a lot of sense to use contravariance for consumer or sender, as for both of them, their type parameter is only used on in-position, so only consumed. I hope you start seeing that the out
modifier is appropriate for type parameters that are only on out-position, so used as result type or read-only property type; and the in
modifier is appropriate for type parameters that are only on in-position, so used as parameter types.
Function types
In function types, there are relations between function types with different expected types of parameters or return types. To see it practically, think of a function that expects as an argument a function accepting an Int
and returning Any
:
Based on its definition, such a function can accept a function of type (Int)->Any
, but it would also work with: (Int)->Number
, (Number)->Any
, (Number)->Number
, (Any)->Number
, (Number)->Int
, etc.
It is because between all those types, there is the following relation:
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.
It is no coincidence. All parameter types in Kotlin function types are contravariant, as the name of this variance modifier in
suggests. All return types in Kotlin function types are covariant, as the name of this variance modifier out
suggests.
In this, just like in many other cases, you do not 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 do not attribute it to generic type modifiers, but they feel using Kotlin or using 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 result and read-only property type) should be covariant, so have the out
modifier. Type parameters that are only used for public in-positions (function parameter type) should be contravariant, so have an in
modifier.
This is the end of the first part. In the next part, you will learn about an important pattern named Covariant Nothing Object. Stay tuned.
In this chapter, I assume that you know what a type is and that you have some basic understanding of generic classes and functions. As a reminder: The type parameter is a placeholder for a type, so T
in class Box<T>
or in fun a<T>() {}
. The type argument is the actual type used when a class is created, or a function is called, so Int
in Box<Int>()
or a<Int>()
. 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>
.