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>.

class Box<T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // Error: Type mismatch val bp: Box<Puppy> = Box<Dog>() // Error: Type mismatch val bn: Box<Number> = Box<Int>() // Error: Type mismatch val bi: Box<Int> = Box<Number>() // Error: Type mismatch }

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>.

class Box<out T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // OK val bp: Box<Puppy> = Box<Dog>() // Error: Type mismatch val bn: Box<Number> = Box<Int>() // OK val bi: Box<Int> = Box<Number>() // Error: Type mismatch }

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>.

class Box<in T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // Error: Type mismatch val bp: Box<Puppy> = Box<Dog>() // OK val bn: Box<Number> = Box<Int>() // Error: Type mismatch val bi: Box<Int> = Box<Number>() // OK }

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?

interface Animal { fun pet() } class Cat(val name: String) : Animal { override fun pet() { println("$name says Meaw") } } fun petAnimals(animals: List<Animal>) { for (animal in animals) { animal.pet() } } fun main() { val cats: List<Cat> = listOf(Cat("Mruczek"), Cat("Puszek")) petAnimals(cats) // Can I do that? } 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. interface Animal class Cat(val name: String) : Animal class Dog(val name: String) : Animal fun addAnimal(animals: MutableList<Animal>) { animals.add(Dog("Cookie")) } fun main() { val cats: MutableList<Cat> = mutableListOf(Cat("Mruczek"), Cat("Puszek")) addAnimal(cats) // COMPILATION ERROR val cat: Cat = cats.last() // If code would compile, it would break here } 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. interface Sender<T : Message> { fun send(message: T) } interface Message interface OrderManagerMessage : Message class AddOrder(val order: Order) : OrderManagerMessage class CancelOrder(val orderId: String) : OrderManagerMessage interface InvoiceManagerMessage : Message class MakeInvoice(val order: Order) : OrderManagerMessage 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. class GeneralSender( serviceUrl: String ) : Sender<Message> { private val connection = makeConnection(serviceUrl) override fun send(message: Message) { connection.send(message.toApi()) } } val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender(ORDER_MANAGER_URL) val invoiceManagerSender: Sender<InvoiceManagerMessage> = GeneralSender(INVOICE_MANAGER_URL) For that, we need a sender type with a contravariant parameter, so it needs the in modifier. interface Sender<in T : Message> { fun send(message: T) } 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. class Consumer<in T> { fun consume(value: T) { println("Consuming$value") } } fun main() { val numberConsumer: Consumer<Number> = Consumer() numberConsumer.consume(2.71) // Consuming 2.71 val intConsumer: Consumer<Int> = numberConsumer intConsumer.consume(42) // Consuming 42 val floatConsumer: Consumer<Float> = numberConsumer floatConsumer.consume(3.14F) // Consuming 3.14 val anyConsumer: Consumer<Any> = Consumer() anyConsumer.consume(123456789L) // Consuming 123456789 val stringConsumer: Consumer<String> = anyConsumer stringConsumer.consume("ABC") // Consuming ABC val charConsumer: Consumer<Char> = anyConsumer charConsumer.consume('M') // Consuming M }

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:

fun printProcessedNumber(transformation: (Int) -> Any) { println(transformation(42)) }

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.

val intToDouble: (Int) -> Number = { it.toDouble() } val numberAsText: (Number) -> String = { it.toString() } val identity: (Number) -> Number = { it } val numberToInt: (Number) -> Int = { it.toInt() } val numberHash: (Any) -> Number = { it.hashCode() } printProcessedNumber(intToDouble) printProcessedNumber(numberAsText) printProcessedNumber(identity) printProcessedNumber(numberToInt) printProcessedNumber(numberHash)

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.

Kotlin type hierarchy

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.

1:

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>.