0

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.

enter image description here

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 ?>:

enter image description here

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...)

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I feel like this question is totally out of my expertise level, but how `capture of ?` is different than `Any?`/`Nothing` of Kotlin? Is there something that we can do with `capture of ?`, but we can't with Kotlin equivalent? For example, we still can't do this in Java: `foo.consumeT(foo.returnT())`. For me `capture of ?` sounds like typical, Java-ish gibberish that was just named better in Kotlin :-P But it is quite probable I miss some understanding of what is going on in this case. – broot Mar 29 '22 at 20:01
  • @broot No they are not very different. Maybe I wasn't clear enough, but my question here is mainly about *how* Kotlin renames these "gibberish" capture types (Kotlin clearly still has capture types!) to meaningful things like `Any?` and `Nothing`. I can't find this process documented anywhere. I at least want a name so I can start my search :) – Sweeper Mar 29 '22 at 20:10
  • @broot I know it probably depends on *where* the capture type is (return type, parameter type, etc), and the type projection (`in`, `out`, `*`, nothing), but I can't describe this exact process in words. – Sweeper Mar 29 '22 at 20:16
  • But isn't it as simple as that the `*` depending on the variance is "converted" to these types? This is explained specifically here: https://kotlinlang.org/docs/generics.html#star-projections "For `Foo`, (...) `Foo<*>` is equivalent to `Foo`. For `Foo`, (...) `Foo<*>` is equivalent to `Foo`. – broot Mar 29 '22 at 20:23
  • @broot The use of a star projection here might have made it seem like I don't understand star projections... The question applies equally to other projections. If I say `Foo`, `x.consumeT` would suddenly take `Nothing`. I understand intuitively that this is because `Foo` is now a "producer of `T`s", so it doesn't take in `T`s anymore. However, in a more complicated case like `aMoreComplicatedCase` (or even more complicated than that), I cannot predict what its parameter type will be for `x: Foo` until I see IntelliJ's autocomplete. – Sweeper Mar 29 '22 at 20:38
  • After I see the answer, I can usually reason (with some handwaving) why it is this way, but I want to know the process by which Kotlin does this "capture types -> actual types" transformation, so that I can actually predict this and explain the process to others. – Sweeper Mar 29 '22 at 20:42

0 Answers0