Search code examples
kotlinsealed-class

How to get rid of this boilerplate code in this sealed class hierarchy?


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 ?


Solution

  • 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.