Kotlin custom Scope function return type not behaving as expected

Viewed 83

this custom function call's a lambda block when null. I expected the function definition below to enforce the same return type. : T
Is their a way to enforce that be block returns type T ?

inline fun <T> T?.whenNull(block: () -> T): T {
    if (this == null) {
        return block() //accepts any return type
    } else {
        return this
    }
}

fun main() {
    val x : Int? = null
    println(x ?: 42)
    println(x.whenNull { 42 })
    println(x.whenNull { "why is kotlin not enforcing return of the same type?" })
}
3 Answers

T in the second whenAll call is being inferred as Any. Imagine that all occurrences of T are replaced Any, the call would be valid, wouldn't you agree? Int and String are both subtypes of Any, after all.

inline fun Any?.whenNull(block: () -> Any): Any {
    if (this == null) {
        return block()
    } else {
        return this
    }
}

fun main() {
    println(x.whenNull { "why is kotlin not enforcing return of the same type?" })
}

Basically, the Kotlin compiler is "trying too hard" here to make your code compile, and infers an unexpected type for your type parameter.


There exists an internal annotation @kotlin.internal.InputTypesOnly that would prevent your code from compiling if the type inferred is not mentioned in one of the input types (parameter types, receiver type, etc) of the function.

In this case, the input type is just Int?, and T is inferred to be Any, so it would be make your code not compile as expected. Unfortunately though, this annotation is internal, and you cannot use it :( KT-13198 is the ticket about making it public.

Interestingly, when passing this to the block the type is preserved and works as expected.

inline fun <T> T?.whenNullAlt(block: (T?) -> T): T {
    if (this == null) {
        return block(this) // "this" is superfluous, but enforces same return type
    } else {
        return this
    }
}

fun main() {
    val x : Int? = null
    println(x.whenNullAlt { 42 })
    println(x.whenNullAlt { "does not compile" })  
}

As other answers pointed out, this is technically correct from Kotlin viewpoint because it can infer T to Any and it would compile.

However, while technically correct, this is a known problem in Kotlin language and is going to be fixed some day. That noted, let's try to understand why your answers's code works while your questions' doesn't.

The reason for this is that lambdas are always inferred last: imagine it as being a queue on what parts of the expression need to have their types inferred and anything inside a lambda is always at the end of the queue, no matter where the lambda is in the expression. So, when going over your example, it infers everything else, decides that the type of this should be Int?, than goes to the lambda, sees a String return type and merges them into Any being very proud of itself.

In the other example, however, the lambda is passed an external fact about it's parameter — the already inferred Int from the receiver (lambda is always the last to get the info, remember?). That way the inference inside the lambda fails because the argument and the result type are in disagreement.

Related