4

Type Hierarchy

open class Fruit()

open class CitrusFruit : Fruit()

class Orange : CitrusFruit()

Declaration-site Variance

The Crate is used as a producer or consumer of Fruits.

Invariant class

class Crate<T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer allowed
}

Covariant classout

class Crate<out T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer not allowed: Error
    fun last(): T = elements.last()    // Producer allowed
}

Contravariant classin

class Crate<in T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer not allowed: Error
}

Use-site Variance

All these use-site projections are for the invariant class Crate<T> defined above.

No Projection

No subtyping allowed: Only the Crate<Fruit> can be assigned to a Crate<Fruit>.

fun main() {
    val invariantCrate: Crate<Fruit> = Crate<Fruit>(mutableListOf(Fruit(), Orange()))

    invariantCrate.add(Orange())       // Consumer allowed
    invariantCrate.last()              // Producer allowed
}

Covariant Projectionout

Subtyping allowed: Crate<CitrusFruit> can be assigned to Crate<Fruit> when CitrusFruit is a subtype of Fruit.

fun main() {
    val covariantCrate: Crate<out Fruit> = Crate<CitrusFruit>(mutableListOf(Orange()))

    covariantCrate.add(Orange())       // Consumer not allowed: Error
    covariantCrate.last()              // Producer allowed
}

Contravariant Projectionin

Subtyping allowed: Crate<CitrusFruit> can be assigned to Crate<Orange> when the CitrusFruit is a supertype of Orange.

fun main() {
    val contravariantCrate: Crate<in Orange> = Crate<CitrusFruit>(mutableListOf(Orange()))

    contravariantCrate.add(Orange())   // Consumer allowed
    contravariantCrate.last()          // Producer allowed: No Error?
}

Questions

  1. Is my understanding and the use of type projection correct in the given example?

  2. For contravariance: why is the last()(producer) function not allowed at declaration-site but allowed at use-site? Shouldn't the compiler show an error like it shows in the declaration-site example? Maybe I'm missing something? If the producer is allowed for contravariance only at use-site, what could be the use case for it?


I prefer detailed answers with examples but any kind input will be much appreciated.

Yogesh Umesh Vaity
  • 41,009
  • 21
  • 145
  • 105

2 Answers2

1

My guess is that the difference between declaration-site and use-site contravariance is that delcaration-site can be statically checked by the compiler, but when using projections there is always the original, unprojected object in existence at run-time. Therefore, it is not possible to prevent the creation of the producer methods for in projections.

When you write:

class Crate<in T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer not allowed: Error
}

The compiler can know at compile-time that no method on Crate<T> should exist that produces a T, so the definition of fun last(): T is invalid.

But when you write:

val contravariantCrate: Crate<in Orange> = Crate<CitrusFruit>(mutableListOf(Orange()))

What has actually been created is a Crate<Any?>, because generics are erased by the compiler. Although you specified that you don't care about producing an item, the generic-erased Crate object still exists with the fun last(): Any? method.

One would expect the projected method to be fun last(): Nothing, in order to give you a compiler-time error if you try to call it. Perhaps that is not possible because of the need for the object to exist, and therefore be able to return something from the last() method.

Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
  • Thanks for the response. I'm trying to understand the reasoning provided in your answer. Can you please elaborate more on the parts: `need for the object to exist` and `original, unprojected object in existence` with examples? – Yogesh Umesh Vaity Nov 14 '20 at 18:44
1

Let's start with the use-site.

When you write

val contravariantCrate: Crate<in Orange> = ...

the right side could be a Crate<Orange>, Crate<Fruit>, Crate<Any?>, etc. So the basic rule is that any use of contravariantCrate should work if it had any of these types.

In particular, for all of them

contravariantCrate.last()

is legal (with type Orange, Fruit, and Any? respectively). So it's legal for Crate<in Orange> and has type Any?.

Similarly for covariantCrate; calling the consumer method technically is allowed, just not with Orange. The problem is that a Crate<Nothing> is a Crate<out Fruit>, and you couldn't do

val covariantCrate: Crate<Nothing> = ...
covariantCrate.add(Orange())

Instead the parameter type is the greatest common subtype of Fruit, CitrusFruit, Nothing, etc. which is Nothing. And

covariantCrate.add(TODO())

is indeed legal because the return type of TODO() is Nothing (but will give warnings about unreachable code).

Declaration-site in or out effectively say that all uses are in/out. So for a contravariant class Crate<in T>, all calls to last() return Any?. So you should just declare it with that type.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • So, you are saying it's a rule in Kotlin that producer and consumer methods for contravariance and covariance respectively are allowed to be called at use-site but are disallowed to be defined at declaration-site? – Yogesh Umesh Vaity Nov 15 '20 at 11:08
  • It seems so, though the documentation says otherwise. Maybe it changed? – Alexey Romanov Nov 15 '20 at 12:45
  • 1
    Purely as a guess, the reason could be to help with porting Java code, because it behaves the same (as far as I know). – Alexey Romanov Nov 15 '20 at 18:15