Home > Software design >  How does Kotlin's data class copy idiom look for nullable types?
How does Kotlin's data class copy idiom look for nullable types?

Time:06-30

I have some code which looks like this, where param is of a data class type:

val options = if (param.language == null) { 
                   param.copy(language = default()) 
              } else { 
                   param 
              }

Now, however, the language object has been moved into a hierarchy of nullable objects, so the check must look like this:

if (param.subObj?.nextObj?.language == null) { ... }

How do I use the copy idiom in this case?

CodePudding user response:

One way to do this is:

val newParam = when {
    param.subObj == null -> param.copy(subObj = SubObj(nextObj = NextObj(language = Language())))
    param.subObj.nextObj == null -> param.copy(subObj = param.subObj.copy(nextObj = NextObj(language = Language())))
    param.subObj.nextObj.language == null -> param.copy(subObj = param.subObj.copy(nextObj = param.subObj.nextObj.copy(language = Language())))
    else -> param
}

I agree that this doesn't look very clean but this seems to be the only way to me, because at each step you need to check if the current property is null or not. If it is null, you need to use the default instance otherwise you need to make a copy.

CodePudding user response:

Could you do something like this?

// you could create a DefaultCopyable interface if you like

data class SubObj(val prop1: Double? = null, val nextObj: NextObj? = null) {
    fun copyWithDefaults() =
        copy(prop1 = prop1 ?: 1.0, nextObj = nextObj?.copyWithDefaults() ?: NextObj())
}

data class NextObj(val name: String? = null) {
    fun copyWithDefaults() = copy(name = name ?: "Hi")
}

I think you need a special function because you're not using the standard copy functionality exactly, you need some custom logic to define defaults for each class. But by putting that function in each of your classes, they all know how to copy themselves, and each copy function that works with other types can just call their default-copy functions.

The problem there though is:

fun main() {
    val thing = SubObj(3.0)
    val newThing = thing.copyWithDefaults()
    println("$thing\n$newThing")
}

> SubObj(prop1=3.0, nextObj=null)
> SubObj(prop1=3.0, nextObj=NextObj(name=null))

Because nextObj was null in SubObj, it has to create one instead of copying it. But the real default value for name is null - it doesn't know how to instantiate one with the other defaults, that's an internal detail of NextObj. You could always call NextObj().copyWithDefaults() but that starts to look like a code smell to me - why isn't the default value for the parameter the actual default value you want? (There are probably good reasons, but it might mean there's a better way to architect what you're up to)

  • Related