article banner (priority)

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

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

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

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 }

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?

interface Animal { fun pet() } class Cat(val name: String) : Animal { override fun pet() { println("$name says Meow") } } 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? 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.

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 }

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.

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

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

interface Sender<in T : Message> { fun send(message: T) }

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.

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

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

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.

Kotlin type hierarchy

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!

1:

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