1

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?

Felix Dombek
  • 13,664
  • 17
  • 79
  • 131
  • Do you have default values for all these nullable types? Like above you used `language = default()` if `langauge` is `null`. What is `subObj` is `null`, do you have a default value for it? – Arpit Shukla Jun 29 '22 at 01:44
  • @ArpitShukla Ah, no, they are supposed to be empty -- default-constructed, if you want. – Felix Dombek Jun 29 '22 at 01:46
  • I mean, if they are `null` what value do you want to assign them? – Arpit Shukla Jun 29 '22 at 01:47
  • I want to assign an object which has just this one property set. – Felix Dombek Jun 29 '22 at 01:48
  • Do all of the intermediates have multiple properties? Like does `subObj` have properties other than `nextObj`? And when you want to create a default instance, you want those other properties to remain null, right? – Arpit Shukla Jun 29 '22 at 01:49
  • Can you share the structure of these data classes? – Arpit Shukla Jun 29 '22 at 01:50
  • `data class SubObj(val prop1: Double? = null, val nextObj: NextObj? = null)` etc. Yes, all of the intermediates have multiple properties, and those should remain null. – Felix Dombek Jun 29 '22 at 01:53
  • Okay, one last question. So if `subObj` has a non-null `prop1` and `nextObj` is `null`, you want to update `nextObj` to a default value, keeping `prop1` intact, right? – Arpit Shukla Jun 29 '22 at 01:58
  • Indeed. I can of course write a cascade of if clauses for it; hoping there is something more elegant that I don't see at the moment. – Felix Dombek Jun 29 '22 at 02:00

2 Answers2

2

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.

Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
2

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)

cactustictacs
  • 17,935
  • 2
  • 14
  • 25