Suppose I've got a sealed class
hierarchy like that:
sealed class A {
abstract val x: Int
abstract fun copyX(x1: Int): A
}
data class A1(override val x: Int, val s1: String) : A() {
override fun copyX(x1: Int): A {
return this.copy(x = x1)
}
}
data class A2(override val x: Int, val s2: String) : A() {
override fun copyX(x1: Int): A {
return this.copy(x = x1)
}
}
All the data classes have field x
and should provide method copyX(x1: Int)
to copy all the fields but x
and override x
with x1
. For instance,
fun foo(a: A): A { a.copyX(100) }
The definitions above probably work but the repeating copyX
across all the data classes seem very clumsy. How would you suggest get rid of this repeated copyX
?
First, you can implement copyX
as an extension (or even A
's member) so as to concentrate the code in one place and avoid at least duplicating the copyX
function in the sealed class subtypes:
sealed class A {
abstract val x: Int
}
fun A.copyX(x1: Int): A = when (this) {
is A1 -> copy(x = x1)
is A2 -> copy(x = x1)
}
data class A1(override val x: Int, val s1: String) : A()
data class A2(override val x: Int, val s2: String) : A()
If you have a lot of sealed subtypes and all of them are data
classes or have a copy
function, you could also copy them generically with reflection. For that, you would need to get the primaryConstructor
or the function named copy
from the KClass
, then fill the arguments for the call, finding the x
parameter by name and putting the x1
value for it, and putting the values obtained from component1()
, component2()
etc. calls or leaving the default values for the other parameters. It would look like this:
fun A.copyX(x1: Int): A {
val copyFunction = this::class.memberFunctions.single { it.name == "copy" }
val args = mapOf(
copyFunction.instanceParameter!! to this,
copyFunction.parameters.single { it.name == "x" } to x1
)
return copyFunction.callBy(args) as A
}
This works because callBy
allows omitting the optional arguments.
Note that it requires a dependency on kotlin-reflect
and works only with Kotlin/JVM. Also, reflection has some performance overhead, so it's not suitable for performance-critical code. You could optimize this by using the Java reflection (this::class.java
, getMethod(...)
) instead (which would be more verbose) and caching the reflection entities.