Is it possible to make safe inline Optional in Kotlin?

457 Views Asked by At

In Kotlin sometimes I have to work with double nullability. For example, I need double nullability, when I want to use T? where T may be a nullable type. There are a few approaches for doing this:

  1. Holder<T>? where Holder is data class Holder<out T>(val element: T) - example1
  2. boolean flag variable - example1
  3. containsKey for Map<K, T?> - example1
  4. The special UNINITIALIZED_VALUE for representing the second kind of null - example1

The last approach has the best performance, but it's also the most error-prone. So I've decided to encapsulate it in inline class Optional<T>:

inline class Optional<out T> @Deprecated(
    message = "Not type-safe, use factory method",
    replaceWith = ReplaceWith("Optional.of(_value)")
) constructor(private val _value: Any?) {
    val value: T?
        get() =
            @Suppress("UNCHECKED_CAST")
            if (isPresent) _value as T
            else null

    val isPresent: Boolean
        get() = _value != NULL

    companion object {
        @Suppress("DEPRECATION")
        fun <T> of(value: T) = Optional<T>(value)

        fun <T : Any> ofNullable(value: T?): Optional<T> =
            if (value == null) EMPTY
            else of(value)

        @Suppress("DEPRECATION")
        val EMPTY = Optional<Nothing>(NULL)
    }

    private object NULL
}

inline fun <T> Optional<T>.ifPresent(code: (T) -> Unit) {
    @Suppress("UNCHECKED_CAST")
    if (isPresent) return code(value as T)
}

inline fun <T> Optional<T>.or(code: () -> T): T {
    ifPresent { return it }
    return code()
}

The first problem with this Optional is public constructor, which allows creating instances with arguments of not matching type.

The second problem was noticed at testing time. Here is the failed test:

emptyOr { Optional.EMPTY }.value assertEql null
fun <T> emptyOr(other: () -> T): T = Optional.EMPTY.or(other)

Exception:

Exception ClassCastException: Optional$NULL cannot be cast to Optional
    at (Optional.kt:42) // emptyOr { Optional.EMPTY }.value assertEql null

If I remove inline modifier from Optional, the test will pass.

Q: Is there any way to fix these problems without removing inline modifier from Optional?


1 Examples include some context. Please read them fully before writing that I added incorrect links.

1

There are 1 best solutions below

0
On

I implemented exactly the same utility in one of my projects: OptionalValue.kt. My implementation is very similar to yours, it is also an inline/value class, so it should be cpu/memory efficient and it passes all tests I throw at it.

Regarding your first question: about a public constructor. There is an annotation specifically for this case: @PublishedApi. I tried to reproduce ClassCastException from your example, but it worked for me without problems, so I believe it was a bug in Kotlin itself (?).

Also, to answer the question why do we need double nullability, I explained my point here