https://kotlinlang.org logo
Title
d

David Kubecka

02/23/2023, 3:36 PM
How to flatten a potentially null object to arguments of a data class constructor (with default values)?
data class Coordinates(val x: Int, val y: Int)

// NOTE: I cannot change this as it comes from 3rd party system.
data class CoordinatesAndColor(val color: String, val x: Int = 0, val y: Int = 0)

// is actually passed from outside so it can be null
val coords: Coordinates? = Coordinates(1, 1)

// now I would like to do something akin to this (in "pseudo-code")
CoordinatesAndColor(color = "red").apply {
  // if coords is null the default values should be used
  coords?.let {
    x = it.x
    y = it.y
  }
}

// currently the only way I know is this
CoordinatesAndColor(color = "red", x = coords?.x ?: 0, y = coords?.y ?: 0)
Any ideas?
j

Joffrey

02/23/2023, 3:44 PM
I would probably use composition instead of redefining the
x
and
y
properties, which solves the problem altogether. It would also allow to use methods/extensions of
Coordinates
more easily with
CoordinatesAndColor
instances. You could still define
x
and
y
extension properties that delegate to the underlying coordinates object.
d

David Kubecka

02/23/2023, 3:47 PM
Yeah, I should have mentioned that I cannot modify
CoordinatesAndColor
(updating the post)
j

Joffrey

02/23/2023, 4:00 PM
Then I'm afraid that you don't really have a choice. Either you repeat and hardcode the default values for
x
and
y
like in your last example, or you use an
if
statement to call the 1-arg constructor or the 3-arg constructor based on the nullilty of `coords`:
// custom factory function
fun CoordsAndColor(color: String, coords: Coordinates?) = if (coords == null) {
    CoordsAndColor(color = color)
} else {
    CoordsAndColor(color = color, x = coords.x, y = coords.y)
}
k

Klitos Kyriacou

02/23/2023, 4:15 PM
Possibly the generated
copy
function could be used:
CoordinatesAndColor(color = "red").let {
    if (coords == null)
        it
    else
        copy(x = it.x, y = it.y)
}
If it's actually more complicated, with more parameters and more conditions, you might want to create a builder.
j

Joffrey

02/23/2023, 4:17 PM
Good point, I didn't think of that. It must be noted that we create 2 instances in this case, which might be problematic if the use case is performance sensitive (which might happen with geometry stuff)
k

Klitos Kyriacou

02/23/2023, 4:21 PM
Indeed, in which case, your solution is better. The real question is, why do we have to repeat the default values like this line in the original question:
CoordinatesAndColor(color = "red", x = coords?.x ?: 0, y = coords?.y ?: 0)
It could have been useful if the language allowed something like:
CoordinatesAndColor(color = "red", x = coords?.x ?: default, y = coords?.y ?: default)
j

Joffrey

02/23/2023, 4:27 PM
Yep, I think there was a YouTrack issue for this. I have needed this a couple times in the past
k

Klitos Kyriacou

02/23/2023, 4:42 PM
j

Jacob

02/23/2023, 4:47 PM
val coords = coordsFromOutside() ?: Coordinates(0,0)
CoordinatesAndColor("red", coords.x, coords.y)
k

Klitos Kyriacou

02/23/2023, 4:48 PM
This repeats the default values (0). If the default values change, you'll have to change them here too.
d

David Kubecka

02/24/2023, 9:45 AM
The solution by @Klitos Kyriacou is perfectly acceptable to me (using scope functions, of course). The geometry stuff was just an example; in reality, the code in question is not on the hot path.
j

Joffrey

02/24/2023, 5:23 PM
Ok. Although I personally find it more readable with a simple
if
and 2 different constructor calls depending on the case. Using a combination of nested `let`/`if`/`copy` is a lot of cognitive overhead IMO.
d

David Kubecka

02/25/2023, 9:17 AM
I think it depends on how many and how complicated are the mandatory arguments. In my case there are a few of them, so the version with
copy
leads to less code duplication.