Search code examples
genericskotlincontravariance

Why are contravariant type parameters in function parameters considered in "out" position?


Hard for me to describe in english, but here's the issue:

class Consumer<in T> {
    fun consume(t: T) {}
}

class Accepter<in T>() {
    // ERROR: Type parameter T is declared as 'in' but occurs in 'out' position in type Consumer<T>
    fun acceptWith(value: T, consumer: Consumer<T>) {}
}

It can be fixed like this:

fun <U : T> acceptWith(value: T, consumer: Consumer<U>) {}

But I don't understand the issue. It doesn't seem unsafe to allow Consumer<T>. Can someone explain this?


Solution

  • Function parameters which themselves allow input are logically equivalent to return values for a function, which are obviously in "out" position.

    Consider this simple example:

    interface Worker<in T> {
        fun work(output: Consumer<T>)
    }
    

    This is logically equivalent to

    interface Worker<in T> {
        fun work(): T
    }
    

    work() can output a value in either case.

    An example of this failing:

    fun bad(anyWorker: Worker<Any>) {
        val stringWorker: Worker<String> = anyWorker
        stringWorker.work(Consumer { value: String -> /* value could be Any since it came from anyWorker! */ })
    }
    

    However, we can solve this by introducing a new type parameter for the function:

    interface Worker<in T> {
        fun <U : T> work(output: Consumer<U>)
    }
    

    Now, work() will only be allowed to call the Consumer with some specific subtype of T that the consumer must be able to consume. For example, lets imagine that work takes another argument, as in the original question, and actually does something:

    class Worker<in T> {
        private val inputs = mutableListOf<T>()
    
        fun <U : T> work(input: U, output: Consumer<U>) {
            inputs += input
            output.accept(input)
        }
    }
    

    By introducing the type parameter U, we can ensure that input and output are consistent with respect to each other, but still allow Worker<Any> to extend Worker<String>.