8
votes

I want a class which is equivalent to Java Optional but also

  • Properly handles null value ("Not set" state is different from "Null set")
  • Is mutable
  • Uses Kotlin built-in null-safety, type parameter can be either nullable or non-nullable which affects all methods.

Non-working code:

class MutableOptional<T> {
    private var value: T? = null
    private var isSet: Boolean = false

    fun set(value: T)
    {
        this.value = value
        isSet = true
    }

    fun unset()
    {
        isSet = false
        value = null
    }

    fun get(): T
    {
        if (!isSet) {
            throw Error("Value not set")
        }
        return value!! // <<< NPE here
    }
}

fun f()
{
    val opt = MutableOptional<Int?>()
    opt.set(null)
    assertNull(opt.get())
}

The problem is that if I try to set null, get() call fails with null pointer exception (caused by !! operator).

Some not-working proposals:

  • Do not use members of type "T?" in such class. I would not use it if I knew how to leave them uninitialized (not allowed by the compiler) or how to make them to have default initialization.
  • Use "fun get(): T?" (with nullable result). I want the result type to have the same nullability as the class type parameter. Otherwise there is no meaning in such null-safety if it is lost in a simple generic class, and I will need to set !! manually where I am sure it is non-nullable (the thing the compiler should ensure), making my code looking like wedge-writing.

Note: This example is synthetic, I do not really need the mutable optional, it is just a simple and understandable example, illustrating a problem I encounter occasionally with Kotlin generics and null-safety. Finding solution to this particular example will help with many similar problems. Actually I have a solution for immutable version of this class but it involves making interface and two implementation classes for present and non-present values. Such immutable optional can be used as type of "value" member but I think it's quite big overhead (accounting also wrapper object creation for each set()) just to overcome the language constraints.

2
Better practice: you shouldn't handle null, but null should only be handled if the generic type is null. That is, value should have type T, but then users can make a MutableOptional<Foo?> if they want to allow nullable Foos. - Louis Wasserman
Yes, this exactly the requirement for MutableOptional class described in the question. And the problem with making value of type T is also mentioned - you cannot initialize it to something. However, now I see that it can be initialized to null as T which is the second variant of the implementation, among with the accepted answer. - vagran

2 Answers

8
votes

The compiler wants you to write code that will be type-safe for all possible T, both nullable and not-null (unless you specify a not-null upper bound for the type parameter, such as T : Any, but this is not what you need here).

If you store T? in a property, it is a different type from T in case of not-null type arguments, so you are not allowed to use T and T? interchangeably.

However, making an unchecked cast allows you to bypass the restriction and return the T? value as T. Unlike the not-null assertion (!!), the cast is not checked at runtime, and it won't fail when it encounters a null.

Change the get() function as follows:

fun get(): T {
    if (!isSet) {
        throw Error("Value not set")
    }
    @Suppress("unchecked_cast")
    return value as T
}
0
votes

I got a similar issue. My use case was to differentiate null and undefined value when I deserialize JSON object. So I create an immutable Optional that was able to handle null value. Here I share my solution:

interface Optional<out T> {
    fun isDefined(): Boolean
    fun isUndefined(): Boolean
    fun get(): T
    fun ifDefined(consumer: (T) -> Unit)

    class Defined<out T>(private val value: T) : Optional<T> {
        override fun isDefined() = true
        override fun isUndefined() = false
        override fun get() = this.value
        override fun ifDefined(consumer: (T) -> Unit) = consumer(this.value)
    }

    object Undefined : Optional<Nothing> {
        override fun isDefined() = false
        override fun isUndefined() = true
        override fun get() = throw NoSuchElementException("No value defined")
        override fun ifDefined(consumer: (Nothing) -> Unit) {}
    }
}

fun <T> Optional<T>.orElse(other: T): T = if (this.isDefined()) this.get() else other

The trick: the orElse method have to be defined as an extension to not break the covariance, because Kotlin does not support lower bound for now.

Then we can define a MutableOptional with no cast in the following way:

class MutableOptional<T> {

    private var value: Optional<T> = Optional.Undefined

    fun get() = value.get()
    fun set(value: T) { this.value = Optional.Defined(value) }
    fun unset() { this.value = Optional.Undefined }
}

I am happy with my immutable Optional implementation. But I am not very happy with MutableOptional: I dislike the previous solution based on casting (I dislike to cast). But my solution creates unnecessary boxing, it can be worst...