7

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 ?

Michael
  • 41,026
  • 70
  • 193
  • 341
  • May I ask what your real-life usecase is for this? I've had similar problems in the past, and I usually either repeated the declarations like this, or found a complete other way. It looks like you want to abstract the subclasses by using this sealed class, and yet you still want to retain the subclass's implementation when copying, so I wonder what your need is exacly. – Joffrey Aug 27 '19 at 08:33
  • @Joffrey I am afraid the actual use case is a bit too complicated to describe now. Thanks for asking though. – Michael Aug 28 '19 at 07:26

1 Answers1

5

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.

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • 1
    I don't like runtime reflection so I won't probably use it but thank you for the detailed answer anyway :)) – Michael Aug 28 '19 at 07:28