1

Kotlin Generic Variance Modifiers

 1 year ago
source link: https://kt.academy/article/ak-variance
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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. 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>.

xxxxxxxxxx
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
}
Target platform: JVMRunning on kotlin v.1.8.10

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

xxxxxxxxxx
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
}
Target platform: JVMRunning on kotlin v.1.8.10

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

xxxxxxxxxx
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
}
Target platform: JVMRunning on kotlin v.1.8.10

Those variance modifiers are illustrated in the below diagram:

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Fvariance.png&w=3840&q=75

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?

xxxxxxxxxx
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?
}
Target platform: JVMRunning on kotlin v.1.8.10

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.

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Flist_covariant.png&w=3840&q=75

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.

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Fmutable_list_definition.png&w=3840&q=75

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.

xxxxxxxxxx
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
}
Target platform: JVMRunning on kotlin v.1.8.10

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.

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

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

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

xxxxxxxxxx
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
}
Target platform: JVMRunning on kotlin v.1.8.10

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:

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

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

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Ffunction_type_hierarchy.png&w=3840&q=75

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.

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Ftyping_system.png&w=3840&q=75
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.

image?url=https%3A%2F%2Fmarcinmoskala.com%2Fadvanced-kotlin-book%2Fmanuscript%2Fresources%2Ffunction_type_modifiers.png&w=3840&q=75

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK