How does this compile? (type assignment issue?)

I was working at my day job and hit a type problem I wasn’t expecting. For the life of me, I cannot figure out what the compiler is doing to get this to work. I was able to recreate it in the Kotlin playground. https://pl.kotl.in/BtaavI0MBX

Here is the code to reproduce it as well (so you don’t have to go to Kotlin Playground)

fun <T> eq(value: T): (T) -> Boolean {
    return { it == value }
}

val asdf: (String) -> Boolean = eq(123) // This is an Int not a String

fun main() {
    println(asdf("Hello, world!!!"))
}

The furthest I have gotten is that it is using T as Comparable<*> but that still makes no sense to me. So this line also compiles.

val asdf: (String) -> Boolean = eq<Comparable<*>>(123)

How do you assign (Comparable<*>) -> Boolean to (String) -> Boolean? A Comparable<*> is not a String.

I think T gets detected as Any for eq.

Try the following to confirm:

val asdf: (String) -> Boolean = eq<Any>(123) 
val withAny: (String) -> Boolean = eq<Any>(123) // Works
val withComparable: (String) -> Boolean = eq<Comparable<*>>(123) // Works
val withInt: (String) -> Boolean = eq<Int>(123) // Fails

I still don’t get how this would work. The set of inputs withAny takes is a superset of the required type (String) -> Boolean, but then withComparable is a subset. I think I am slowly losing my mind.

interface A
class B: A
class C: A

val afun: (B) -> Boolean = eq(C()) // Works
1 Like

When you write a type like (A) -> B, it is basically just syntactic sugar for something like Function<A, B>, where Function is an interface with a declaration like this:

interface Function<in P, out R> {
    operator fun invoke(p: P): R
}

Notice that the type parameter P is declared in.What this means is that Function<P1, R> is assignable to Function<P2, R> if P2 is assignable to P1 (i.e. the subtype relation is backwards for the in type arguments). That is why a value of type (Any) -> Boolean can be assigned to a variable of type (String) -> Boolean.

You can read more about ins and outs of generic variance in the section about Declaration-site variance here

Ok. Ok. So like Varia said, any function that takes in some super type of the value will work. What is confusing about this from my perspective is how we got to Comparable<*>. So by not defining T in eq, we let the compiler play this game with T. It knows the value coming in is Int, but it can’t use that, so it finds a common parent Comparable<*>. It can use that for the function and since a function that takes in Comparable<*> can also take in an Int. So the whole thing is really.

val viacompiler: (String) -> Boolean = eq(123 /* as Comparable<*> by compiler */)

The fact that I want T to be Int doesn’t really matter to the compiler because there is nothing to force the value passed in to eq to not be a subclass.

This is incredibly frustrating from my perspective. It is a huge let down when the type system goes :+1: ,when in reality, it will never work. Part of this is due to the implicit in and out on function which I wish was more explicit.

Please note that the problem existed in the first place, because you created the code that is confusing to the compiler:

val foo: (String) -> Boolean = eq(123)

In more usual case it won’t compile, as you expect:

val bar = eq(123)
val foo: (String) -> Boolean = bar // compile error

Having said that, your expected behavior that the T is exactly the same as the passed param, is indeed a valid use case and it would be useful to have such a feature in Kotlin. As a matter of fact, Kotlin team themselves encountered similar problems when implementing stdlib, so the Kotlin compiler already supports this case, but… it is kept as an internal feature, it is hidden.

To enable this feature stdlib uses @kotlin.internal.Exact annotation. Your function could be defined as:

fun <T> eq(value: @Exact T): (T) -> Boolean {

And then it would behave as you expect. Unfortunately, for now this is reserved for stdlib only, I’m not sure why.

1 Like

Yep, this works (playground):

@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
fun <T> eq(value: @kotlin.internal.Exact T): (T) -> Boolean {
    return { it == value }
}

val asdf: (String) -> Boolean = eq(42) // Error :)

fun main() {
    println(asdf("Hello, world!!!"))
}
1 Like