Why are contravariant type parameters in function parameters considered in “out” position?

#1

Also posted at stack overflow: https://stackoverflow.com/questions/55961416/why-are-contravariant-type-parameters-in-function-parameters-considered-in-out

It’s a little hard to describe, but basically Kotlin is complaining that a contravariant type paramter is used in an “out” position when it’s used as a contravariant argument to another class:

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?

#2

The argument position is called contravariant because its variance goes in contrary direction w.r.t. the class variance. It means that the supertypes of a class can take the subtypes of an argument type as a parameter and vice versa.

Let’s consider some actual parameter type S. In this example a type Accepter<S>, which is a supertype of Accepter<Any>, must take a subtype of Consumer<Any> as a parameter, but with the given signature it takes Consumer<S>, that isn’t a subtype of Consumer<Any>, but rather a supertype of it.

Another example why that signature would be unsafe if allowed. Let’s consider the following implementations of Accepter and Consumer:

class AnyAccepter : Accepter<Any>() {
    override fun acceptWith(value: Any, consumer: Consumer<Any>) {
        consumer.consume(Any())
    }
}

class StringConsumer : Consumer<String>() {
    override fun consume(t: String) {
        println(t.length)
    }
}
fun main() {
    val anyAccepter = AnyAccepter()
    val stringAccepter: Accepter<String> = anyAccepter
    stringAccepter.acceptWith("x", StringConsumer())
}

With these implementations you’ll get an unsound program, which will result in ClassCastException at run time:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Object cannot be cast to class java.lang.String 
	at contravariance.StringConsumer.consume(consumers.kt:27)
	at contravariance.AnyAccepter.acceptWith(consumers.kt:23)
	at contravariance.ConsumersKt.main(consumers.kt:36)