Variance in Kotlin: A Beginner’s Guide

Variance is a concept that describes how types relate to each other when they have type parameters. For example, if Dog is a subtype of Animal, is List<Dog> a subtype of List<Animal>? The answer depends on the variance of the type parameter of List.

In this blog, we will explore the different kinds of variance in Kotlin and how they affect the type system and the code we write. We will also compare them with Java’s wildcard types and see how Kotlin simplifies the syntax and semantics of generics.

Declaration-site variance

One way to achieve variance in Kotlin is by using declaration-site variance. This means that we can specify the variance of a type parameter at the class level, where it is declared. This affects all the members and fields of the class that use that type parameter.

For example, let’s define a simple class that represents a producer of some type T:

class Producer<T>(val value: T) { fun produce(): T = value }

This class has a type parameter T that appears as the return type of the produce() method. Now, let’s say we have two subtypes of AnimalDog and Cat. We can create instances of Producer<Dog> and Producer<Cat>:

val dogProducer = Producer(Dog()) val catProducer = Producer(Cat())

But can we assign a Producer<Dog> to a variable of type Producer<Animal>? Intuitively, this should be possible, because a producer of dogs is also a producer of animals. We can always get an animal from it by calling produce(). However, if we try to do this in Kotlin, we get a compiler error:

val animalProducer: Producer<Animal> = dogProducer // Error: Type mismatch

This is because by default, generic types in Kotlin are invariant, meaning that they are not subtypes of each other, even if their type arguments are. This is similar to how Java behaves without wildcards.

To fix this error, we need to make the type parameter T covariant, meaning that it preserves the subtype relationship. We can do this by adding the out modifier to the type parameter declaration:

class Producer<out T>(val value: T) { fun produce(): T = value }

The out modifier tells the compiler that T is only used as an output, not as an input. This means that we can only return values of type T from the class, but we cannot accept them as parameters. This ensures that we don’t violate the type safety by putting a wrong value into the class.

With this modifier, we can now assign a Producer<Dog> to a Producer<Animal>, because Producer<Dog> is a subtype of Producer<Animal>:

val animalProducer: Producer<Animal> = dogProducer // OK

This is called covariance, because the subtype relationship varies in the same direction as the type argument. If Dog is a subtype of Animal, then Producer<Dog> is a subtype of Producer<Animal>.

Covariance is useful when we want to read values from a generic class, but not write to it. For example, Kotlin’s standard library defines the interface List<out T> as covariant, because we can only get elements from a list, but not add or remove them. This allows us to assign a List<Dog> to a List<Animal>, which is convenient for polymorphism.

Use-site variance

Another way to achieve variance in Kotlin is by using use-site variance. This means that we can specify the variance of a type parameter at the point where we use it, such as in a function parameter or a variable declaration. This allows us to override the default variance of the class or interface where the type parameter is declared.

For example, let’s define another simple class that represents a consumer of some type T:

class Consumer<T>(var value: T) { fun consume(value: T) { this.value = value } }

This class has a type parameter T that appears as the parameter type of the consume() method. Now, let’s say we have two subtypes of AnimalDog and Cat. We can create instances of Consumer<Dog> and Consumer<Cat>:

val dogConsumer = Consumer(Dog()) val catConsumer = Consumer(Cat())

But can we assign a Consumer<Animal> to a variable of type Consumer<Dog>? Intuitively, this should be possible, because a consumer of animals can also consume dogs. We can always pass a dog to it by calling consume(). However, if we try to do this in Kotlin, we get a compiler error:

val dogConsumer: Consumer<Dog> = animalConsumer // Error: Type mismatch

This is because by default, generic types in Kotlin are invariant, meaning that they are not subtypes of each other, even if their type arguments are. This is similar to how Java behaves without wildcards.

To fix this error, we need to make the type parameter T contravariant, meaning that it reverses the subtype relationship. We can do this by adding the in modifier to the type parameter usage:

val dogConsumer: Consumer<in Dog> = animalConsumer // OK

The in modifier tells the compiler that T is only used as an input, not as an output. This means that we can only accept values of type T as parameters, but we cannot return them from the class. This ensures that we don’t violate the type safety by getting a wrong value from the class.

With this modifier, we can now assign a Consumer<Animal> to a Consumer<Dog>, because Consumer<Animal> is a subtype of Consumer<Dog>:

val dogConsumer: Consumer<in Dog> = animalConsumer // OK

This is called contravariance, because the subtype relationship varies in the opposite direction as the type argument. If Dog is a subtype of Animal, then Consumer<Animal> is a subtype of Consumer<Dog>.

Contravariance is useful when we want to write values to a generic class, but not read from it. For example, Kotlin’s standard library defines the interface MutableList<T> as invariant, because we can both get and set elements in a mutable list. However, if we only want to add elements to a list, we can use the function addAll(elements: Collection<T>), which accepts a collection of any subtype of T. This function uses use-site variance to make the parameter type covariant:

fun <T> MutableList<T>.addAll(elements: Collection<out T>)

This allows us to add a List<Dog> to a MutableList<Animal>, which is convenient for polymorphism.

Comparison with Java

If you are familiar with Java’s generics, you might notice some similarities and differences between Kotlin and Java’s variance mechanisms. Java uses wildcard types (? extends T and ? super T) to achieve covariance and contravariance, respectively. Kotlin uses declaration-site variance (out T and in T) and use-site variance (T and in T) instead.

The main advantage of Kotlin’s approach is that it simplifies the syntax and semantics of generics. Wildcard types can be confusing and verbose, especially when they are nested or combined with other types. Declaration-site variance allows us to specify the variance once at the class level, instead of repeating it at every usage site. Use-site variance allows us to override the default variance when needed, without introducing new types.

Another advantage of Kotlin’s approach is that it avoids some of the limitations and pitfalls of wildcard types. For example, wildcard types cannot be used as return types or in generic type arguments. Declaration-site variance does not have this restriction, as long as the type parameter is used consistently as an output or an input. Use-site variance also allows us to use both covariant and contravariant types in the same context, such as in function parameters or variables.

Conclusion

In this blog, we learned about variance in Kotlin and how it affects the type system and the code we write. We saw how declaration-site variance and use-site variance can help us achieve covariance and contravariance for generic types. We also compared them with Java’s wildcard types and saw how Kotlin simplifies the syntax and semantics of generics.

Variance is an important concept for writing generic and polymorphic code in Kotlin. It allows us to express more precise and flexible types that can adapt to different situations. By understanding how variance works in Kotlin, we can write more idiomatic and effective code with generics.

I hope you enjoyed this blog and learned something new. If you have any questions or feedback, please let me know in the comments below. Thank you for reading! 😊

%d bloggers like this: