Broken null checking for Optionals?

43 Views Asked by At

I've encountered a situation where Kotlin's nullable type checking is doing the opposite of what I'd expect. Have I misunderstood something fundamental, or is the following not expected behavior?

The return of getOrNull for an Optional of a non-nullable type can be checked for null without the compiler complaining.

class Compiles_TypeNonNullable {
    fun foo(o : Optional<String>): String {
        val result = o.getOrNull() ?: "fallback"
        return result
    }
}

While trying to add a null check when the Optional is for a nullable type results in a compile error.

class DoesNotCompile_TypeNullable {
    fun foo(o : Optional<String?>): String {
        val result = o.getOrNull() ?: "fallback"
        return result
    }
}
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: 
[ERROR] public fun <T : Any> Optional<TypeVariable(T)>.getOrNull(): TypeVariable(T)? defined in kotlin.jvm.optionals

This is the precise opposite of what I'd expect.

The above was observed with both Kotlin 1.9.10 and 1.9.23.

2

There are 2 best solutions below

0
Sweeper On BEST ANSWER

getOrNull only works on Optional<T>s where T is non-nullable. This is clear in it's declaration:

public fun <T : Any> Optional<T>.getOrNull(): T? = orElse(null)

The : Any constraint disallows any nullable types for T. : Any isn't technically needed here. The declaration still compiles without it, and it will return the wrapped non-null value or null if the optional is empty. But in Kotlin, Optional<T?> just doesn't make sense. The type constraint not only makes the intended usage clear, it also discourage people from using Optional<T?>.

Here is why Optional<String?> doesn't make sense.

An Optional<T> either:

  • contains an instance of T, or;
  • is empty

This makes sense for Optional<String> - it either contains a string or is empty. getOrNull returns the string if it is not empty, and returns null otherwise.

According to the same logic, an Optional<String?> in Kotlin can be any of the following:

  • contains a string
  • contains null
  • is empty

The thing is, Optional cannot represent that second case. Internally, there is just a field of type T. If this field is null, the optional is empty, otherwise the optional contains an instance of T. Optional by design cannot distinguish between the second and third cases.

To represent all 3 cases, you would need a Optional<Optional<String>> or similar.

2
Leviathan On

In your first example, the Optional can be in any of these two states:

  1. Contains a String
  2. Is empty (as in "missing" as one would expect for something that's optional)

Now, getOrNull works as expected: It returns the String in the first case and it returns null in the second.

In your second example, the Optional can be in any of these three states:

  1. Contains a String
  2. Is empty
  3. Contains null

That's what Kotlin sees, anyways. Since Optional is a Java type and Java's type system doesn't differentiate between nullable and non-nullable types, the second and the third cases are the same for Java, because internally, the Optional uses null to represent a missing value (case 2).

For Kotlin to correctly handle the last two cases, it would need to differentiate between them. Since that's not possible (the information just doesn't exist), it refuses to execute getOrNull at all. That is done by declaring that function to just work for non-nullable types:

public fun <T : Any> Optional<T>.getOrNull(): T?

T is of type Any, not of type Any?. That's why your second example won't compile, it is by design. And with good reason.

If you, as a developer, decide that to differentiate between cases two and three isn't important, you can always use this instead:

val result = o.orElse(null) ?: "fallback"

Edit: From the comments to the question it seems unlikely that you can construct an Optional with a nullable Kotlin type anyways. So all of this may be a moot point after all. You can delcare an Optional<String?>, but you'll only ever see a concrete object of the subtype Optional<String>.