I found something in Kotlin that had always been intuitive to me, but I recently realised I can't explain the low level details of this.
Consider what the return type of x.returnT, and the parameter types of consumeT and aMoreComplicatedCase would be in the below code:
val x: Foo<*> = TODO()
class Foo<T> {
fun returnT(): T = TODO()
fun consumeT(t: T) {}
fun aMoreComplicatedCase(func: (T) -> T) {}
}
IntelliJ tells me that x.returnT returns Any?, consumeT takes Nothing, and aMoreComplicatedCase takes (Any?) -> Nothing.
Using the spec, we know that * roughly means out Any? and in Nothing at the same time, and applying PECS (Or maybe this should be called POCI in Kotlin since producer out consumer in), we know x is a producer of Any? and a consumer of nothing.
An even more intuitive explanation is that * means "we know nothing about what goes in the <> of Foo at all", and because of that, of course returnT can return anything, even nullable things. Similarly, of course consumeT can't take anything - it takes T, the exact thing we don't know.
Similar arguments can be applied to cases where it is out SomeType or in SomeType.
Where I am stuck
Having been programming in Java for a long time, I did not expect this result at all. I expected something similar to what IntelliJ would say for the similar Java code:
public class Main {
public static void main(String[] args) {
Foo<?> foo = new Foo<>();
}
}
class Foo<T> {
public T returnT() { return null; }
public void consumeT(T t) { }
public void aMoreComplicatedCase(Function<T, T> func) { }
}
IntelliJ says that x.returnT returns a capture of ?, consumeT takes a capture of ?, and aMoreComplicatedCase takes Function<capture of ?, capture of ?>:
I looked around the Kotlin spec and found that Kotlin also have captured types, which are fresh type variables with certain bounds. So where does Nothing and Any? come from? Or in the case of using in SomeType as the projection, consumeT will take a SomeType instead. Why is it SomeType, but not a capture type with SomeType as its subtype? After all, the spec says in the type capturing section:
- For a contravariant type argument Ai
in Ai, if Fi is a covariant type parameter, Ki is an ill-formed type. Otherwise, Ki :> Ai.- For a bivariant type argument
⋆,kotlin.Nothing<: Ki <:kotlin.Any?
I'm guessing something else happens after type capturing? What is that process called? Or is there a totally different process that is not type capturing going on here?
(I guess there is also the possibility that this is just a matter of "what string representation IntelliJ/kotlinc uses for display, when the type is a captured type" and has nothing to do with the type system itself...)

