1

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.

IlyaMuravjov
  • 2,352
  • 1
  • 9
  • 27
  • Why would you want double nullability in the first place? – marstran Oct 06 '19 at 11:37
  • @marstran check out examples [here](https://stackoverflow.com/questions/58001124/what-is-the-most-concise-way-to-include-functions-or-lambdas-as-conditions-in-a/58191904#58191904) (`Holder?`), [here](https://stackoverflow.com/questions/58233684/update-field-when-others-fields-are-motifed/58236171#58236171) (`boolean initialized`), [here](https://stackoverflow.com/questions/58213918/kotlin-how-to-create-dynamic-object/58219723#58219723) (`containsKey`) and [here](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Lazy.kt) (`UNINITIALIZED_VALUE`) – IlyaMuravjov Oct 06 '19 at 11:57
  • Sure, but I would argue there are better ways to design those things than using nulls for everything. The last example with `UNINITIALIZED_VALUE` is a good example. Using the internal object makes the intent much more clear. Using `containsKey` is also much clearer than first checking some holder for null, then checking the value for null. – marstran Oct 06 '19 at 12:43
  • Anyway, for your question, that exception seems to me like it is a bug with inline classes. You should report an issue on https://youtrack.jetbrains.com/issues/KT. – marstran Oct 06 '19 at 12:55

1 Answers1

0

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

broot
  • 21,588
  • 3
  • 30
  • 35