Skip to main content

Grokking Kotlin generics: in and out

Today I struggled to understand Kotlin's in and out modifiers while learning about generics. Fortunately, I think I've finally developed an intuition around it.

Suppose we have an Animal supertype, a Cat and Dog subtype, and a Box type:

open class Animal
class Cat : Animal()
class Dog : Animal()
class Box<TAnimal : Animal>(    private var animal: TAnimal) {    fun getAnimal(): TAnimal = animal
    fun setAnimal(animal: TAnimal) {        this.animal = animal    }}

And suppose we have a function that accepts a Box<Animal> as an argument:

fun setAnimalFree(box: Box<Animal>) {    // do something}

Would it be possible to call setAnimalFree with a Box<Cat> object?

fun main() {  val catInABox = Box(Cat())  setAnimalFree(catInABox)  // ERROR: Type mismatch. Required: Box<Animal>. Found: Box<Cat>}

Unfortunately, the above code results in an type mismatch error. Intuitively, we might think that Box<Cat> satisfies Box<Animal> just because Cat satisfies Animal. That is, we expect Box<Cat> to be a subtype of Box<Animal>. However, this thinking is incorrect. Let's see why.

Imagine that we have a client who works with Box<Animal>. The client has the following expectations:

  1. They can call setAnimal(animal) with any Animal. In the following example, the client expects to set a Dog.
  2. They can call getAnimal() to receive any Animal.
fun doClientStuff(box: Box<Animal>) {    box.setAnimal(Dog())    val animal: Animal = box.getAnimal()    // ...}
fun main() {    val catInABox = Box(Cat())    doClientStuff(catInABox)  // ERROR: Type mismatch. Required: Box<Animal>. Found: Box<Cat>}

Same situation as before. We might postulate that Box<Cat> cannot satisfy Box<Animal> because we cannot call .setAnimal(Dog()) on it. We cannot set a Dog on a box for cats, but we can do so for a box for any animals!

On the other hand, the client expects .getAnimal() to return any animal that satisfies the Animal type. Box<Cat> meets this expectation because it returns a Cat, which satisfies Animal.

Based on this example, we can infer the following:

  1. Methods on Box that consume an Animal type must receive a type that minimally satisfies every possible Animal type because the client may want to set every possible animal.
  2. Conversely, methods on Box that produce an Animal type may return a subset of the Animal type because the client just expects any animal.

The Ins and Outs of Generic Variance in Kotlin explains this logic quite well.

The only way for Box<Cat> to satisfy Box<Animal> is if we constrain the client to use only the methods on Box<Animal> that produce an Animal type, while banning methods that consume Animal types. We can achieve this specific constraint using the out modifier!

fun doClientStuff(box: Box<out Animal>) {    val animal: Animal = box.getAnimal()    // ...}
fun main() {    val catInABox = Box(Cat())    doClientStuff(catInABox)  // No more error! Box<Cat> satisfies Box<out Animal>!}

Another alternative to using Box<out Animal> would be to define our function generically:

fun <TAnimal : Animal> doClientStuff(box: Box<TAnimal>) {    val animal: TAnimal = box.getAnimal()    // ...}
fun main() {    val catInABox = Box(Cat())    doClientStuff(catInABox)}

If the client takes a Box<Cat> instead and we want to pass a Box<Animal>, what constraint can we apply to allow this? We can use the in modifier, which constrains Box<Cat> to consumer methods only! This works because Box<Animal> will work when called with the consumer method .setAnimal(Cat()). But not with the producer method .getAnimal(), which expects only a Cat to be returned.

fun doClientStuffV2(box: Box<in Cat>) {    box.setAnimal(Cat())    // ...}
fun main() {    val animalInABox = Box(Animal())    doClientStuffV2(animalInABox)}