Question about compiler non-null inference

I’m wondering why this code is accepted by the compiler:

fun <T: Any> getOrDefault(fn: () -> T?, defaultValue: T): T {
  val result = fn()
  val str = result?.toString()
  return if (str.isNullOrBlank()) defaultValue else result
}

But inlining the variable str is not accepted:

fun <T: Any> getOrDefault(fn: () -> T?, defaultValue: T): T {
  val result = fn()
  return if (result?.toString().isNullOrBlank()) defaultValue else result
}

And gives the compiler-error: Type mismatch. Required: T, found: T?.

Why does the not-null inference on result work when the compiler can infer non-null on the intermediate variable str of different type, which is derived from result?
And if that works, then why does it not work when inlining the intermediate variable?

Actually if result is null then defaultValue is returned, not result.

I’m really interested in any comments from JetBrains. It seems like in some cases the compiler can deduce the type of A by checking the type of B. But this is weird that inlining of str changes this. Maybe it is able of such deduction only if A and B are defined as variables, but it doesn’t work for expressions.

this variant with inlined toString() works:

    fun <T: Any> getOrDefault2(fn: () -> T?, defaultValue: T): T {
        val result = fn()
        return if (result == null || result.toString().isBlank()) defaultValue else result
    }

or this one-liner:

    fun <T: Any> getOrDefault3(fn: () -> T?, defaultValue: T): T =
        fn()?.let { if (it.toString().isBlank()) defaultValue else it } ?: defaultValue

I think you totally misunderstood this example. No, if result is null then:

result?.toString().isNullOrBlank()

does not return null, but true. If it would return null then this code would not at all compile, because, as you said, if can’t handle null values in Kotlin.

Also, the main point here isn’t how to write this code to make it work - I’m pretty sure OP knows how to do it. The main point is that both code examples provided by OP are almost exactly the same, but they are inferred to different types by the compiler, which is pretty strange.

1 Like

Huh, you’re right. Should have read the question more precisely, not just the code in it. Learned something though, I obviously have a couple of misconceptions about the ? operator…

To answer the question that was actually asked, then, it’s because result?.toString() is not a smart cast. For that you need an explicit null comparison. That is the reason why luhtonens answer works, because there result has been smart cast from T? to T by the explicit if (result == null … ).

There is a difference between type inference and smart casting, since type inference does not change the type of a reference, but a smart cast does, in a narrowly defined scope. Like for example when explicitly stating if (x == null), x will be smart cast to a non-nullable type in the scope of the else clause. If you state if (x != null), x will be smart cast in the if clause. But it needs this explicit null comparison to trigger the cast. Kotlin won’t go around casting types to something you might not want unless you’re very clear about it.

1 Like

?. does not short-circuit the whole chained expression by returning null. It skips only the current step (toString()), passing null to the next one. Next step is isNullOrBlank() and as its name suggests, it returns true for nulls. It doesn’t throw NPE, because it is not a member function, but extension and extensions can work on null receivers.

This is also not true :stuck_out_tongue_winking_eye: Kotlin can smart-cast to not-null without explicit check for null. It uses contracts to let the compiler know that depending on some circumstances, an expression is/isn’t null. See this:

val nullableString: String? = "foo"
if (!nullableString.isNullOrEmpty()) {
    println(nullableString.length) // smart-cast to String
}

The real magic in OP’s example is that we didn’t check result itself for null, but rather the expression: result?.toString(). But still, in one of these examples the compiler was able to smart cast result which wasn’t directly tested.

And of course, if result?.toString() expression is not null, then result also has to be not null. This is easy to deduce by humans, but not necessarily by the compiler. Technically speaking we smart cast variable A by checking expression B. I have never seen the Kotlin compiler does this and I even requested a similar feature somewhere on this forum. Also, even if the compiler becomes smarter and smarter with time, this still doesn’t answer the question, why it is able to perform such smart-cast in one example and it can’t do this in the equivalent code. I guess this is just a missing piece in this feature.

1 Like