Default value for undefined case

For enums it’s common to have default/undefined value like:

data class User(val gender: Gender = Gender.Undefined)

And then it’s clear for a developer that enum has list of options and there is Undefined one:

if(user.gender == Gender.Undefined) doIfUndefined()
else doIfDefined()

But for classes like Date it doesn’t work because there is no any hint for a developer that there is undefined case. It is still possible to defined undefined date instance:

object UndefinedDate : Date(-1L)

fun Date.isUndefined(): Boolean {
    return this == UndefinedDate
}

class User(val birth: Date = UndefinedDate)

But it’s error prone because developers might use just -1 date as it is.

Is there something better than making such field nullable? Or maybe there is some future language feature which informs developers about default/undefined value?

Just because I’m curious, why does this needs to be undefined instead of absent (null)?
Especially when there are so much features in a language to deal with null?

I asume you have three options to specify undefined:

  • wrap the type: Kotlin does not provide Union-types yet, but the second best is sealed classes.

    sealed class Container<T>(
        protected val value :  T?, 
        val defined : Boolean
    ){
        object Undefined<T : Any> : Container<T>(
             null, defined = false
        )
        class Present<T>(val value : T) : Container<T>(
            value, defined = true
        )
    }
    
  • Change the type: add a specific field which tells if the type is present. Or use sealed classes again. You need to change the class for this, for both cases, every type needs to know about it, so it must be important…

  • Use a specific value for the type: use a specific instance (null-object pattern) or set the value itself to something that doesn’t make sense for the type (eg. -1 for age).
    Then you can add an extension function which checks if it is defined

    fun Person.asDefinedOrNull() : Person? = when(this){
        Person.Undefined -> null 
        else -> this
    }
    
2 Likes

Just to add to the sealed class example, it is possible to reduce the wrapper and use extension functions with contracts to add smart casts for simple if statements like this:

sealed class Container<out T> {
    object Undefined : Container<Nothing>()
    class Defined<T>(val value: T) : Container<T>()
}

@ExperimentalContracts
fun <T> Container<T>.isDefined(): Boolean {
    contract {
        returns(true) implies (this@isDefined is Container.Defined<T>)
    }

    return this is Container.Defined<T>
}

@ExperimentalContracts
fun main() {
    val x: Container<Int> = Container.Undefined

    if (x.isDefined()) {
        println(x.value)
    }
}
3 Likes

One way is to create instance of “undefined” object, and use this instance everywhere as undefined value. Then you’d check for referential equality with === operator.

class C{
    companion object{
        val UNDEFINED_DATE = Date(0)
    }

    fun workOnDate(date: Date) {
        if(date===UNDEFINED_DATE)
            throw IllegalStateException()
    }
}

Another way is to pass nullable value into a function, and filter out null values at beginning, working with non-null value for the rest of function:

    fun workOnDate(inDate: Date?) {
        val date = inDate?:throw IllegalStateException()
    }
1 Like