`@OverloadResolutionByLambdaReturnType` won't work with generics?

I’m trying to run this code and it won’t compile (real-life foo can actually return null):

@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
fun <R> foo(block: () -> R): R? = block()

fun foo(block: () -> Unit) { block() }

val int: Int? = foo { 1 }   // Type mismatch: inferred type is Unit but Int? was expected
val unit: Unit = foo {}

It seems that Kotlin doesn’t recognize that { 1 } returns an Int. Funnily, this works, albeit with a warning:

val int: Int? = foo({ 1 } as (() -> Int))    // Warning: No cast needed

And un-generifying foo works as well:

@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
fun foo(block: () -> Int): Int? = block()

Can I make this work? And why casting the lambda help?

2 Likes

I think the issue here is that the compiler here is automatically assuming that the foo { 1 } call discards the value of 1 instead of returning it. However, after some fiddling around, this works:

fun <R: Any, K: R?> foo(block: () -> R): K = block() as K

fun foo(block: () -> Unit, unit: Unit = Unit) { block() }

val int: Int? = foo { 1 }  
val unit: Unit = foo {}

fun main() {
    println(int.toString() + unit)
}

and the reason that it works is because from the POV of the call resolution algorithm it first tries to pick out the most applicable function by it having exactly the same number of parameters (and so here it goes for the foo with type params first). Then, it tries to infer those type params, but because the return type clearly must extend R?, and Unit doesn’t extend Unit?, it disregards that function completely, and so now it instead goes for the Unit foo function because it can be satisfied with one param because of the default unit parameters. And so yeah the reason that this doesn’t work with the regular <R> foo(block: () -> R): R? is because R? is sort of like concretely applied as in once the compiler picked the <R> foo function it now reports any type mismatches, while with the weird K parameter it keeps in mind that it’s possible that this function just absolutely doesn’t fit at all. This is like Kotlin language weirdness but AFAIK this is actually documented behaviour so this should be future-proof. Oh and also the neat thing about this is that it doesn’t even need the @OverloadResolutionByLambdaReturnType because :star2: magic :star2:

i don’t think this needs the second fun exactly… not sure why but this works and somehow sets what to null, at least on 1.4.30:

fun <R: Any, K: R?> foo(block: () -> R) = null as K
val what: String = foo {}
1 Like

well the second fun in general is not needed at all if you really think about it unless you want different behaviour if the type is Unit

Also yeah the null thing was an oversight on my end because literally any R will work as K since R:R? all the time, but oddly enough that still doesn’t make the trick not work, which is an interesting inconsistency.

In the last example, I would expect R to be Unit and K to be String, so K: R? would be not satisfied. So I don’t quite get how it works or what the actual type of foo becomes.

Also, this works as well on 1.4.+:

fun <R, K: Unit?> foo(block: () -> R) = null as K

But on 1.3 it produces

Type parameter bound for K in fun <R, K : Unit?> foo(block: () → R): K is not satisfied: inferred type String is not a subtype of Unit?

Which I would expect in all cases…

1 Like

It probably upcasted both of these, and so in the last example R was Unit but it upcasted it to Any, and so K was extending Any?, and String is K in this case and it extends Any?, and so it was satisfied. That makes me think tho that it could be possible to fix this using @kotlin.internal.OnlyInputTypes or @kotlin.internal.Exact. I’mma test it out and see what happens.

1 Like