2

Example


We have two simple objects:

Store1(
   var domainId = null
   val localId = 123
)

Store2(
   var domainId = 5
   val localId = null
)

So after merging we get:

var domainId = 5
val localId = 123

Half there solution


fun Store.join(other: Store): Store {
    val store = Store(null, null, null, null, null, 0, "", null, 0, null, null, false)

    for (prop in Store::class.memberProperties) {
        prop.get(store) = prop.get(this) ?: prop.get(other)
    }

    return store
}

Current problems:

  1. prop.get(stopDetailModel) is read only, I can't assign value to it.
  2. It is not a generic function.

Is this achievable?

SKREFI
  • 177
  • 2
  • 13

4 Answers4

3

For data classes, this is a solution without using the Java Class, only KClass (and without mutation).

You can implement such a generic function with a reified type parameter, which is a type parameter that you can get its KClass at runtime. Then you can directly call the constructor since you have a data class. For each parameter to the constructor, you lookup the property with the same name and find the first one whose value of that property is not null.

inline fun <reified T : Any> merge(vararg xs: T): T {
  val cls = T::class
  require(cls.isData) { "Only data class can be merged" }
  return cls.constructors.first().call(* ctor.parameters.map { par ->
    val prop = cls.declaredMemberProperties.first { it.name == par.name }
    val picked = xs.find { prop.get(it) != null }
    picked?.let { prop.get(it) }
  }.toTypedArray())
}

This will work for arbitrarily many instances:

data class Store(
  var domainId: Int?,
  val localId: Int?,
)

val x = merge(
  Store(123, null),
  Store(null, 456),
  Store(null, 789),
)

fun main() {
  println(x) // Store(123, 456)
}
daylily
  • 876
  • 5
  • 11
  • I want to clarify one thing I am confused about. Do all members have to be nullable? – SKREFI Jul 14 '21 at 07:46
  • 2
    If they are not nullable, you won't be able to store `null` at the first place like in your examples. – daylily Jul 14 '21 at 07:47
3

More generic solution (with some limitations)

  • Allows to merge not only data classes but any classes (if there is an appropriate constructor, it may be not a no-arg constructor);
  • Allows to merge both mutable and immutable properties(*see Limitations section), including ones defined in superclasses;
  • No Java Reflection API (only Kotlin Reflection)
inline fun <reified T : Any> mergeOrNull(a: T, b: T): T? {
    val availableConstructors = kClass.constructors.filter { it.visibility != PRIVATE && it.visibility != PROTECTED }
    for (constructor in availableConstructors) {
        return mergeOrNull(a, b, constructor) ?: continue
    }
    return null
}

inline fun <reified T : Any> mergeOrNull(a: T, b: T, constructor: KFunction<T>): T? {
    //Match parameters with properties by name
    val properties = T::class.memberProperties.associateBy { it.name }
    val parameterToPropertyMap = constructor.parameters.associateWith { properties[it.name] }

    //check there is no unmatched mandatory parameters
    // (unmatched parameters with default values can be ignored)
    val unmatchedMandatoryParameters = parameterToPropertyMap
        .filterValues { it == null }.keys
        .filter { !it.isOptional }
    if (unmatchedMandatoryParameters.isNotEmpty()) {
        log.debug(
            "Failed to use $constructor constructor for merge. " +
                    "No matching property for mandatory constructor parameters: ${unmatchedMandatoryParameters.joinToString { it.name!! }}."
        )
        return null
    }
    val mergePropertyValues = { prop: KProperty1<T, *> -> prop.get(a) ?: prop.get(b) }
    val args = parameterToPropertyMap.filterValues { it != null }.mapValues { (_, prop) -> mergePropertyValues(prop!!) }

    val result = try {
        constructor.callBy(args)
    } catch (e: Throwable) {
        log.debug("Failed to use $constructor constructor for merge. ${e.stackTraceToString()}")
        return null
    }
    
    val (mutableProperties, immutableProperties) = T::class.memberProperties.partition { it is KMutableProperty1<T, *> }
    mutableProperties.forEach { (it as KMutableProperty1<T, *>).setter.call(result, mergePropertyValues(it)) }
    
    //check that all immutable properties were merged correctly
    val incorrectlyMergedImmutableProperties = immutableProperties.filter { it.get(result) != mergePropertyValues(it) }
    return when {
        incorrectlyMergedImmutableProperties.isEmpty() -> result
        else -> {
            log.debug("Failed to use $constructor constructor for merge. " +
                    "Failed to correctly merge following immutable properties: ${
                        incorrectlyMergedImmutableProperties.joinToString {
                            "${it.name} (expected: ${mergePropertyValues(it)}, actual: ${it.get(result)})"
                        }
                    }")
            null
        }
    }
}

Limitations

  1. Can't correctly merge immutable properties if there are several properties, evaluated from one constructor parameter when some of them evaluated to null, while some others - not.
data class DataStore(val x: String?) {
    val y = "$x!!!"
}

val result = mergeOrNull(DataStore(null), DataStore("2")) // null
//Failed to use fun <init>(kotlin.String?): DataStore constructor for merge.
//Failed to correctly merge following immutable properties: y (expected: null!!!, actual: 2!!!)
  1. Can't correctly merge immutable properties if there is a same-named constructor parameter, but its value is not assigned as-is.
class DataStore(x: String?) {
    val x = "$x!!!"
}

val result = mergeOrNull(DataStore("x"), DataStore(null)) // null
//Failed to use fun <init>(kotlin.String?): DataStore constructor for merge. 
//Failed to correctly merge following immutable properties: x (expected: x!!!, actual: x!!!!!!)

Update (recursive merge for nested objects)

inline fun <reified T : Any> mergeOrNull(a: T, b: T) = mergeOrNull(a, b, T::class)

fun <T : Any> mergeOrNull(a: T, b: T, kClass: KClass<T>): T? {
    //If a and b are primitives, merge strategy is simple
    if (a is Int || a is Long || a is Double || a is Float || a is Short || a is Char || a is Byte || a is Boolean || a is String || a::class.isValue || a::class.objectInstance != null) return a

    //Otherwise we need to construct new object
    val availableConstructors = kClass.constructors.filter { it.visibility != PRIVATE && it.visibility != PROTECTED }
    for (constructor in availableConstructors) {
        return mergeOrNull(a, b, kClass, constructor) ?: continue
    }
    return null
}

private fun <T : Any> mergeOrNull(a: T, b: T, kClass: KClass<T>, constructor: KFunction<T>): T? {
    //Match parameters with properties by name
    val properties = kClass.memberProperties.associateBy { it.name }
    val parameterToPropertyMap = constructor.parameters.associateWith { properties[it.name] }

    //check there is no unmatched mandatory parameters
    // (unmatched parameters with default values can be ignored)
    val unmatchedMandatoryParameters = parameterToPropertyMap
        .filterValues { it == null }.keys
        .filter { !it.isOptional }
    if (unmatchedMandatoryParameters.isNotEmpty()) {
        log.debug(
            "Failed to use $constructor constructor for merge. " +
                    "No matching property for mandatory constructor parameters: ${unmatchedMandatoryParameters.joinToString { it.name!! }}."
        )
        return null
    }
    val mergePropertyValues = { prop: KProperty1<T, *> ->
        val propA = prop.get(a)
        val propB = prop.get(b)
        @Suppress("UNCHECKED_CAST")
        if (propA != null && propB != null) mergeOrNull(propA, propB, propA::class as KClass<Any>) else propA ?: propB
    }

    val args = parameterToPropertyMap.filterValues { it != null }.mapValues { (_, prop) -> mergePropertyValues(prop!!) }

    val result = try {
        constructor.callBy(args)
    } catch (e: Throwable) {
        log.debug("Failed to use $constructor constructor for merge. ${e.stackTraceToString()}")
        return null
    }

    val (mutableProperties, immutableProperties) = kClass.memberProperties.partition { it is KMutableProperty1<T, *> }
    mutableProperties.forEach { (it as KMutableProperty1<T, *>).setter.call(result, mergePropertyValues(it)) }

    //check that all immutable properties were merged correctly
    val incorrectlyMergedImmutableProperties = immutableProperties.filter { it.get(result) != mergePropertyValues(it) }
    return when {
        incorrectlyMergedImmutableProperties.isEmpty() -> result
        else -> {
            log.debug("Failed to use $constructor constructor for merge. " +
                    "Failed to correctly merge following immutable properties: ${
                        incorrectlyMergedImmutableProperties.joinToString {
                            "${it.name} (expected: ${mergePropertyValues(it)}, actual: ${it.get(result)})"
                        }
                    }")
            null
        }
    }
}
  • Is it possible to make it recursive for nested objects? Let's say Store has a data class Address(street: String, nr: Int) object. This solution would take **a**'s address but I want mergeOrNull(a.address, b.address) instead. I used a nested for loop to achieve this but only on a specific object and I don't know how this would work with a template. Recursivity with class.isPrimitive() maybe, and go deeper each time if the data is not primitive I suppose. – SKREFI Jul 16 '21 at 08:01
  • 1
    Yes, `mergePropertyValues` lambda should be tweaked to make a recursive call, but inline functions in Kotlin [can't be recursive](https://stackoverflow.com/questions/48812060/kotlin-recursive-extension-function-to-walk-android-view-hierarchy), so `mergeOrNull` needs to be rewritten with the [following trick](https://stackoverflow.com/a/58998901/13968673), and also the merge of primitive types needs to be handled. Updated my answer. – Михаил Нафталь Jul 16 '21 at 12:58
3

Even more generic solution (overcomes all limitations of the previous solution):

  • Allows to merge any classes (if there is at least one constructor without mandatory non-nullable interface/functional parameters; regardless of its accessibility);
  • Allows to merge all properties, both mutable and immutable, including ones defined in superclasses;
  • Merge is always successful (if not fall into an infinite recursive loop)

Price for this:

  • Usage of Java Reflection API aka "dark magic" (so, will work only for JVM, moreover only for JDK < 12)

Bonus:

  • Merge strategy for properties extracted into an argument
  • Several items could be passed for merging
  • No reified generics
fun <T : Any> merge(
    vararg x: T,
    propertiesMergeStrategy: (a: T, b: T, KProperty1<T, *>) -> Any?
): T = x.reduce { acc, t -> mergeUsingDarkMagic(acc, t, propertiesMergeStrategy) }

fun <T : Any> mergeUsingDarkMagic(
    a: T, b: T, propertiesMergeStrategy: (a: T, b: T, KProperty1<T, *>) -> Any?
): T {
    @Suppress("UNCHECKED_CAST")
    val aKlass = a::class as KClass<T>
    //If a and b are primitives, merge strategy is simple
    if (a is Int || a is Long || a is Double || a is Float || a is Short || a is Char || a is Byte || a is Boolean || a is String || aKlass.isValue || aKlass.objectInstance != null || aKlass.java.isArray) return a

    //Need to create new class instance otherwise
    return aKlass.createDummyInstance().apply {
        val (mutableProperties, immutableProperties) = aKlass.memberProperties.partition { it is KMutableProperty1<T, *> }

        //Handle a situation, when properties of `a` are absent in `b`
        // (it may happen if they both implement the same interface, but have different runtime classes)
        val bKlass = b::class
        val bKlassProperties by lazy { bKlass.memberProperties.toSet() }
        val propertiesMerge = { prop: KProperty1<T, *> ->
            if (aKlass == bKlass || bKlassProperties.contains(prop)) propertiesMergeStrategy(a, b, prop)
            else prop.get(a)
        }

        //set mutable properties using basic Kotlin Reflection API
        mutableProperties.forEach {
            //bypass private modifier
            it.isAccessible = true

            (it as KMutableProperty1<T, *>).setter.call(this, propertiesMerge(it))
        }
        //set immutable properties using Java Reflection API via direct access to the underlying class fields
        immutableProperties.forEach {
            val javaField = it.javaField ?: return@forEach
            //bypass private modifier
            it.isAccessible = true

            //bypass final modifier; works only for JDK < 12
            if ((javaField.modifiers and FINAL) == FINAL) {
                val modifiersField = Field::class.java.getDeclaredField("modifiers")
                modifiersField.isAccessible = true
                modifiersField.setInt(javaField, javaField.modifiers and FINAL.inv())
            }

            javaField.set(this, propertiesMerge(it))
        }
    }
}

fun <T : Any> KClass<T>.createDummyInstance(): T {
    if (this.java.isPrimitive) {
        @Suppress("UNCHECKED_CAST")
        return when (this) {
            Int::class -> 0
            Long::class -> 0L
            Double::class -> 0.0
            Float::class -> 0.0f
            Short::class -> 0.toShort()
            Char::class -> 0.toChar()
            Byte::class -> 0.toByte()
            Boolean::class -> false
            else -> 0
        } as T
    } else if (this.java.isArray) {
        @Suppress("UNCHECKED_CAST")
        return when (this) {
            IntArray::class -> intArrayOf()
            LongArray::class -> longArrayOf()
            DoubleArray::class -> doubleArrayOf()
            FloatArray::class -> floatArrayOf()
            ShortArray::class -> shortArrayOf()
            CharArray::class -> charArrayOf()
            ByteArray::class -> byteArrayOf()
            BooleanArray::class -> booleanArrayOf()
            else -> emptyArray<Any>()
        } as T
    }

    //Take the first that comes to hand constructor without mandatory non-nullable interface/functional parameters (god knows how to instantiate them via reflection)
    val constructor = constructors.firstOrNull {
        it.parameters.all { param -> param.isOptional || param.type.isMarkedNullable || (param.type.jvmErasure !is Function<*> && !param.type.jvmErasure.java.isInterface) }
    } ?: throw NoSuchElementException("Failed to instantiate $this; no constructor without mandatory non-nullable interface/functional parameters")
    val args = constructor.parameters
        .filter { !it.isOptional } //Omit optional parameters
        .associateWith {
            when {
                it.type.isMarkedNullable -> null //Pass null for nullable parameters
                else -> it.type.jvmErasure.createDummyInstance() //Recursively instantiate mandatory non-nullable parameters
            }
        }
    return constructor.apply { isAccessible = true }.callBy(args)
}

Simple merge strategies could be passed as a lambda:

//Now it works!
println(merge(DataStore(null), DataStore("2")) { a, b, prop -> prop.get(a) ?: prop.get(b) }) //DataStore(x=2, y='null!!!')

data class DataStore(val x: String?) {
    val y = "$x!!!"

    override fun toString() = "DataStore(x=$x, y='$y')"
}

Recursive merge strategies require declaring them as a function (and passing as a function reference):

fun <T> mergeRecursively(a: T, b: T, prop: KProperty1<T, *>): Any? {
    val propA = prop.get(a)
    val propB = prop.get(b)
    @Suppress("UNCHECKED_CAST")
    return if (propA != null && propB != null) mergeUsingDarkMagic(propA, propB, propA::class as KClass<Any>, ::mergeRecursively) else propA ?: propB)
}

//Now it works too!
println(merge(DataStore("x"), DataStore(null), propertiesMergeStrategy = ::mergeRecursively)) //DataStore(x='x!!!')

class DataStore2(x: String?) {
    val x = "$x!!!"

    override fun toString() = "DataStore2(x='$x')"
}
  • "mergeUsingDarkMagic" good name indeed. The code is wizardy to me and I will have to analyze it, I have got a question, what happens to lists? (mostly Lists of Objects (not primitives)) – SKREFI Jul 19 '21 at 06:25
  • 1
    `List` is an interface and it turned out, that merge of interfaces was not addressed. Interfaces don't have constructors, moreover, it may happen that both merging items have the same interface type, but different implementation classes (with some extra properties, not defined in the interface/another implementation). Updated my answer with the fix for this case (and several other bug fixes). Also, added some clarifying comments. – Михаил Нафталь Jul 19 '21 at 18:35
  • 1
    Now lists can be merged! Lists (depending on their implementation) store their values in some underlying data structures - they will be merged following the same merge strategy for their fields as all other classes. So, `null` and `arrayListOf(null, 2)` will be merged to `arrayListOf(null, 2)`. – Михаил Нафталь Jul 19 '21 at 18:39
2

Generic approach could be achieved by using Reified Type Parameters. So declare following function:

inline fun <reified T> merge(a: T, b: T): T {
    val result = T::class.java.newInstance()

    val properties = T::class.declaredMembers.filterIsInstance<KMutableProperty<*>>()
    properties.forEach {
        val value = it.getter.call(a) ?: it.getter.call(b)
        it.setter.call(result, value)
    }

    return result
}

And call it directly

val result = merge(store1, store2)
println("domainId=" + result.domainId + ", localId=" + result.localId)

or using extension function

fun Store.merge(that: Store): Store {
    return merge(this, that)
}

val result = store1.merge(store2)
println("domainId=" + result.domainId + ", localId=" + result.localId)

Both code samples will output

domainId=5, localId=123

Note: this solution is only suitable for classes with entirely mutable properties (var) and NOT suitable for immutable (val) properties!

Nikolai Shevchenko
  • 7,083
  • 8
  • 33
  • 42
  • How does this work for immutable properties? – daylily Jul 14 '21 at 07:40
  • Thank you, amazing code! One more thing, having two lists of stores, on list with localId and one list, the response from the server with stores that contain the domainId. I want to zip them, and use this function on each ziped element. [1,2,3] zip [1,2,3] -> [merge(1,1), merge(2,2)...] I would rather not assume they come in the same order even if they do, but instead zip based on a custom field which I know both have (timestamp). How would I do that? Should a ask a separate question? Didn't find anything online either. Or maybe of have a better solution? – SKREFI Jul 14 '21 at 07:43
  • 1
    @SKREFI you can first sort both list by the timestamp and then zip. – daylily Jul 14 '21 at 07:54
  • @daylily yeah... it won't work with immutable properties. If there are any of them then the only suitable solution is invoking the all-arg constructor (like you showed in your Answer). However your solution is limited to data-classes, while my solution is suitable for regular classes – Nikolai Shevchenko Jul 14 '21 at 08:09