Variance follows the direction values move
A producer returning T can often be covariant with out T. A consumer accepting T can often be contravariant with in T. A mutable container that both accepts and returns values generally needs invariance.
interface Source<out T> { fun next(): T }
interface Sink<in T> { fun accept(value: T) }
fun consume(source: Source<String>, sink: Sink<Any>) {
val broadSource: Source<Any> = source
val textSink: Sink<String> = sink
textSink.accept(broadSource.next().toString())
}
A source of strings can safely stand in for a source of arbitrary objects because every produced string is an object. A sink capable of accepting any object can accept strings. Allowing a mutable list of strings to be treated as a mutable list of objects would permit inserting an integer into it.
Exercise
Implement a string source and an object sink. Try reversing both assignments and inspect the compiler errors.
Check: explain variance through permitted reads and writes instead of memorizing only the keywords.