Kotlin - How to get a property from an object in collection - kotlin

I have never worked before with Kotlin so I have a newbie question. I am working with an existing codebase, so I am wondering about a few things. I see that there is a function getDepartmentById which looks like this:
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
Calling that function returns either Failure or FlatResp. From what I can see in the code FlatResponse is typealias for Map<MainAccessType, Entities>.
The function getOneByIdFlattened looks like this:
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
Also, I wonder why is this function returning a collection, and not just a single object when it should get a single row by id from DB?
This is the sql function:
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""

So there are a lot of things wrong in the snippet provided. Given Snippets:
1
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
2
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
3
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
4
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
Issues:
in snippet 2, jsonObject should be JsonObject(..)
I have no idea what the following lines do :
//snippet1:
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
//snipper 4
either.eager {...block...}
addUpdatedAtEpoch(result)
doQuery(ctx, statement, mapOf(..)
//snippet3
UserRoles.ADMIN -> ""
"""select ${createSelectFields(departmentKeys)}
They are all probably some extension functions or util files made by your company or from some famous libraries like anko orsplitties . plus these are mixes with function calls of your own class, like createSelectFields or ctx.user.courierIds. also if i have to guess, then this seems like an unusual way of performing some operation on an sql dB
based on just code completion by android studio, i have been able to figure out the classes as following:
class Entities
typealias FlatResp = Map<MainAccessType, Entities>
class Ctx
sealed class Either<A,B>(val a:A?, val b:B?){
val value:A? = null
class Left<A>(val aa:A):Either<A,A>(aa,aa)
class Right<B>(val bv:B):Either<B,B>(bb,bb)
}
class Failure
class Row
class JsonObject(val pair:Pair<String,Long>):JSONObject(){
fun right():Either<Long,JsonObject>{
}
}
class jsonObject()
enum class MainAccessType{DEPARTMENT}
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> {
return getOneByIdFlattened(
ctx,
params.right(),
getDepartmentByIdSql(ctx),
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
}
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> {
return getDepartmentById(ctx, JsonObject("id" to id))
}
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> {
return either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
}
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
fun mapFromDbNames(row:Row): Map<String,Any?>{
}
fun identity(param : List<Map<String, Any?>>): List<Map<String, Any?>>{
}
This is still not correct and has a lots of red lines in it. but what you can do is keep this as a starter in a separate file, compare and fix the code accordingly and then maybe we can tell what would be a better way:
replace inline functions (fun xyz(...) = someValue ) to block functions. (alt+enter in windows, cmd+n in mac)
instead of typeAlias, use map directly
::something means a function is passed as parameter . its similar to how we pass runnables in java 8, but even more shorthand. you can do ctrl+click( for mac its cmd+click) on that function and goto that function to check what its params are, what its return type are. do the same for various classes/ extension fucntions, variables too. this will help the most
instead of passing something into something which is being passed into another thing (like val bot = Robot(Petrol("5Litres") ) ) , split into different lines to make it understandable ( val amount = "5litres"; val equipment = Petrol(amount) ; val bot = Robot(equipment) )
try to not use 3rd party library/ replace with your own understandable code.
repeat steps 1-5
Hope this gives someplace to start. kotlin is a beautiful language but is also very easy to make unreadable.

Mapping Map values
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
TL;DR
Without data to work with, here's my guess:
val extractedNames: Map<Long, String?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
when (val name = userData["name"]) {
is String -> name
else -> null
}
}
println(extractedNames)
// {1=Bergen, 2=Cindy, 3=Dave}
Intro
Kotlin is great for manipulating collections. For a more general of how to work with collections in Kotlin, I think the docs are really clear Collection transformation operations#Map.
Let's see how that works for this example. You want to extract a specific element, so for that we can use map().
From your screenshot it looks like this is a Map<Long, Map>, where the value is a Map<String, Any?>. I'll assume you want to change the Map<Long, Map> to a Map<Long, String>, where the key is the database ID and the value is user's name.
Test data
So I've got something to test with, I made a new Map:
val destinationDepartment: Map<Long, Map<String, Any?>> =
mapOf(
1L to mapOf(
"id" to 1,
"name" to "Bergen",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
2L to mapOf(
"id" to 2,
"name" to "Cindy",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
3L to mapOf(
"id" to 3,
"name" to "Dave",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
)
Basic noop
First, set up the basics. A Map can be converted to a list of Entries. When we call map(), it will iterates over each Entry, and applies a lambda - which is something we must write. In this instance, the lambda receives the key and value of the Map, and must return a new value.
Aside: the Java equivalent is map.entrySet().stream().map(...)...
Here, the lambda just returns a pair (created with to).
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
id to userData
}
println(extractedNames)
// Output: [(1, {id=1, name=Bergen, createdAt=2021-08-19T11:00:07.447660, updatedAt=2021-08-19T11:00:07.449969}),
// (2, {id=2, name=Cindy, createdAt=2021-08-19T11:00:07.463813, updatedAt=2021-08-19T11:00:07.463845}),
// (3, {id=3, name=Dave, createdAt=2021-08-19T11:00:07.463875, updatedAt=2021-08-19T11:00:07.463890})]
Pretty boring! But now we're set up for the next step - extracting name from userData: Map<String, Any?>.
Extracting name
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// Output: [(1, Bergen), (2, Cindy), (3, Dave)]
Now there's loads of ways to improve this. Making sure that name is a String, not Any?, filtering out blank or null names, mapping to DTOs, sorting. Again, the Kotlin documentation would be a good start. I'll start by listing one really good improvement.
Converting List<Pair<>> to Map<>
If you look at the type of val extractedNames, you'll see that it's a list, not a map.
val extractedNames: List<Pair<Long, Any?>> = ...
That's because the lambda we wrote in the map() function is returning a Pair<Long, String>. Kotlin doesn't know that this is still considered a Map. We can convert any List<Pair<>> back to a map with toMap()
toMap()
val extractedNames: Map<Long, Any?> = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
.toMap() // convert List<Pair<>> to a Map<>
println(extractedNames)
// Output: {1=Bergen, 2=Cindy, 3=Dave}
But this is also not great. Why is id: Long in the lambda if we're not using it? Because we're only extracting the name from userData, we're only mapping the values of the Map. We don't need id: Long at all. Fortunately Kotlin has another useful method: mapValues() - and it returns a Map<>, so we can drop the toMap(). Let's use it.
mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Umm weird. Why are the ids in the values? That's because the mapValues() lambda should return the new value, and in our lambda we're returning both the id and name - oops! Let's only return the name.
Fixing mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
userData["name"]
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Better! Because id is not used, an underscore can be used instead
Aside: Note that the lambda doesn't have a return. Read Returning a value from a lambda expression for an explanation.

Related

Passing Lamda function to Generic function not working

I am playing with Kotlin and I am trying to convert a working Scala code to Kotlin. Everything seems to go pretty well but the compiler gives me this error and I dont know how to handle it.
Type mismatch: inferred type is Any but ExQuestion was expected for this line: return makeMap(questions, add2)
I am using a generic function because I need to access members of type A when building the map and the members would be visible through the lambda function provided.
Here's the code which you can copy into the Kotlin sandbox:
data class ExQuestion(val area: String, val rId: String, val text: String, val rIdAnswer: String, val line: Long)
fun main() {
fun <A> makeMap(list: List<A>, addValue: (A, MutableMap<String, A>) -> Unit): Map<String, A> {
val map = mutableMapOf<String, A>()
for( item in list) {
addValue(item, map)
}
return map
}
val add2: (ExQuestion, MutableMap<String, ExQuestion>) -> Unit =
{ question: ExQuestion, map: MutableMap<String, ExQuestion> ->
val key = question.rId
if (map[key] == null) {
map[key] = question
} else {
println("Id Frage mehrfach vorhanden - " + key)
}
}
val questions = listOf(ExQuestion("Area", "Q01", "text", "A01",1))
return makeMap(questions, add2)
}
Working code:
data class ExQuestion(val area: String, val rId: String, val text: String, val rIdAnswer: String, val line: Long)
fun main() {
fun <A> makeMap(list: List<A>, addValue: (A, MutableMap<String, A>) -> Unit): Map<String, A> {
val map = mutableMapOf<String, A>()
for( item in list) {
addValue(item, map)
}
return map
}
val add2: (ExQuestion, MutableMap<String, ExQuestion>) -> Unit =
{ question: ExQuestion, map: MutableMap<String, ExQuestion> ->
val key = question.rId
if (map[key] == null) {
map[key] = question
} else {
println("Id Frage mehrfach vorhanden - " + key)
}
}
val questions = listOf(ExQuestion("Area", "Q01", "text", "A01",1))
val map = makeMap(questions, add2)
println(map.values)
}
I'm not sure what your question is, but you can convert your list of questions to a map keyed on rId by doing:
val map = questions.map { it.rId to it }.toMap()
println(map)
Result:
{Q01=ExQuestion(area=Area, rId=Q01, text=text, rIdAnswer=A01, line=1)}
Update in response to comments.
You can achieve that without a mutable map by doing something like this:
val map = questions
.groupBy { it.rId }
.mapValues { (key, values) ->
if (values.size > 1) println("Id Frage mehrfach vorhanden - $key")
values.first()
}
However, I think your mutable map solution is fine and arguably clearer, so this is just for demonstration.

Map Key Values to Dataclass in Kotlin

how can I set properties of a dataclass by its name. For example, I have a raw HTTP GET response
propA=valueA
propB=valueB
and a data class in Kotlin
data class Test(var propA: String = "", var propB: String = ""){}
in my code i have an function that splits the response to a key value array
val test: Test = Test()
rawResp?.split('\n')?.forEach { item: String ->
run {
val keyValue = item.split('=')
TODO
}
}
In JavaScript I can do the following
response.split('\n').forEach(item => {
let keyValue = item.split('=');
this.test[keyValue[0]] = keyValue[1];
});
Is there a similar way in Kotlin?
You cannot readily do this in Kotlin the same way you would in JavaScript (unless you are prepared to handle reflection yourself), but there is a possibility of using a Kotlin feature called Delegated Properties (particularly, a use case Storing Properties in a Map of that feature).
Here is an example specific to code in your original question:
class Test(private val map: Map<String, String>) {
val propA: String by map
val propB: String by map
override fun toString() = "${javaClass.simpleName}(propA=$propA,propB=$propB)"
}
fun main() {
val rawResp: String? = """
propA=valueA
propB=valueB
""".trimIndent()
val props = rawResp?.split('\n')?.map { item ->
val (key, value) = item.split('=')
key to value
}?.toMap() ?: emptyMap()
val test = Test(props)
println("Property 'propA' of test is: ${test.propA}")
println("Or using toString: $test")
}
This outputs:
Property 'propA' of test is: valueA
Or using toString: Test(propA=valueA,propB=valueB)
Unfortunately, you cannot use data classes with property delegation the way you would expect, so you have to 'pay the price' and define the overridden methods (toString, equals, hashCode) on your own if you need them.
By the question, it was not clear for me if each line represents a Test instance or not. So
If not.
fun parse(rawResp: String): Test = rawResp.split("\n").flatMap { it.split("=") }.let { Test(it[0], it[1]) }
If yes.
fun parse(rawResp: String): List<Test> = rawResp.split("\n").map { it.split("=") }.map { Test(it[0], it[1]) }
For null safe alternative you can use nullableString.orEmpty()...

kotlin, how convert a EnumMap into regular Map

need to call a api which take Map<String, String>
fun api.log(map: Map<string, String>
but the key has to be only from the registered ones, so defined a enum for the registered keys:
enum class RegisteredKey {
NONE, ZOOM
}
and first build the EnumMap<> to enforce key type:
var enumParamMap: EnumMap<RegisteredKey, String> = EnumMap<RegisteredKey, String>(RegisteredKey::class.java)
enumParamMap.put(RegisteredKeys.NONE, "0")
enumParamMap.put(RegisteredKeys.ZOOM, "1")
doLog(enumParamMap)
question 1, is there constructor to build the enumMap directly with data?
and then need to transform the EnumMap into a Map<> so that the api.log() will accept it
fun doLog(enumParamMap: EnumMap<RegisteredKey, String>) {
val map: MutableMap<String, String> = mutableMapOf()
for (enum in enumParamMap.entries) {
map.put(enum.key.name, enum.value)
}
api.log(map)
}
question 2: is there simpler way to map the enumMap to regular map?
I'm not sure I'm interpreting your first question correctly, but if you mean you want to initialize an exhaustive EnumMap where every key has an entry, similar to the Array constructor that takes a lambda, you could write one like this:
inline fun <reified K : Enum<K>, V> exhaustiveEnumMap(init: (key: K) -> V): EnumMap<K, V> {
val map = EnumMap<K, V>(K::class.java)
for (key in enumValues<K>())
map[key] = init(key)
return map;
}
Usage:
val map = exhaustiveEnumMap<RegisteredKey, String> { key -> key.ordinal.toString() }
or
val map = exhaustiveEnumMap<RegisteredKey, String> { key ->
when (key){
RegisteredKey.NONE -> "0"
RegisteredKey.ZOOM -> "1"
}
}
Edit based on your comment: You could do that by wrapping a mapOf call with the EnumMap constructor like this, but it would be instantiating an intermediate throwaway LinkedHashMap:
val map = EnumMap(mapOf(RegisteredKey.NONE to "0", RegisteredKey.ZOOM to "1"))
Instead, you could write a helper function like this:
inline fun <reified K: Enum<K>, V> enumMapOf(vararg pairs: Pair<K, V>): EnumMap<K, V> =
pairs.toMap(EnumMap<K, V>(K::class.java))
Usage:
var enumParamMap = enumMapOf(RegisteredKey.NONE to "0", RegisteredKey.ZOOM to "1")
-------
For your next question, I'm not sure if this is really any simpler, but you could do:
fun doLog(enumParamMap: EnumMap<RegisteredKey, String>) =
api.log(enumParamMap.map{ it.key.name to it.value }.toMap())
It's more concise, but you're allocating a list and some pairs that you wouldn't be with your way of doing it.
Something like this should work for initialization:
EnumMap<RegisteredKey, String>(
mapOf(RegisteredKey.NONE to "0", RegisteredKey.ZOOM to "1")
)
To get a normal map just call .toMutableMap():
EnumMap<RegisteredKey, String>(
mapOf(RegisteredKey.NONE to "0", RegisteredKey.ZOOM to "1")
).toMutableMap()

Combining/merging data classes in Kotlin

Is there a way to merge kotlin data classes without specifying all the properties?
data class MyDataClass(val prop1: String, val prop2: Int, ...//many props)
with a function with the following signature:
fun merge(left: MyDataClass, right: MyDataClass): MyDataClass
where this function checks each property on both classes and where they are different uses the left parameter to create a new MyDataClass.
Is this possible possible using kotlin-reflect, or some other means?
EDIT: more clarity
Here is a better description of what i want to be able to do
data class Bob(
val name: String?,
val age: Int?,
val remoteId: String?,
val id: String)
#Test
fun bob(){
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
//TODO: merge without accessing all properties
// val result =
assertThat(result).isEqualTo(Bob(id = "local_id", name = "Ben", age=1, remoteId = "remote_id"))
}
If you want to copy values from the right when values in the left are null then you can do the following:
inline infix fun <reified T : Any> T.merge(other: T): T {
val propertiesByName = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor
?: throw IllegalArgumentException("merge type must have a primary constructor")
val args = primaryConstructor.parameters.associateWith { parameter ->
val property = propertiesByName[parameter.name]
?: throw IllegalStateException("no declared member property found with name '${parameter.name}'")
(property.get(this) ?: property.get(other))
}
return primaryConstructor.callBy(args)
}
Usage:
data class MyDataClass(val prop1: String?, val prop2: Int?)
val a = MyDataClass(null, 1)
val b = MyDataClass("b", 2)
val c = a merge b // MyDataClass(prop1=b, prop2=1)
A class-specific way to combine data classes when we can define the fields we want to combine would be:
data class SomeData(val dataA: Int?, val dataB: String?, val dataC: Boolean?) {
fun combine(newData: SomeData): SomeData {
//Let values of new data replace corresponding values of this instance, otherwise fall back on the current values.
return this.copy(dataA = newData.dataA ?: dataA,
dataB = newData.dataB ?: dataB,
dataC = newData.dataC ?: dataC)
}
}
#mfulton26's solution merges properties that are part of primary constructor only. I have extended that to support all properties
inline infix fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
parameter to (property.get(other) ?: property.get(this))
}
val mergedObject = primaryConstructor.callBy(args)
nameToProperty.values.forEach { it ->
run {
val property = it as KMutableProperty<*>
val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
property.javaSetter!!.invoke(mergedObject, value)
}
}
return mergedObject
}
Your requirements are exactly the same as copying the left value:
fun merge(left: MyDataClass, right: MyDataClass) = left.copy()
Perhaps one of use isn't properly understanding the other. Please elaborate if this isn't what you want.
Note that since right isn't used, you could make it a vararg and "merge" as many as you like :)
fun merge(left: MyDataClass, vararg right: MyDataClass) = left.copy()
val totallyNewData = merge(data1, data2, data3, data4, ...)
EDIT
Classes in Kotlin don't keep track of their deltas. Think of what you get as you're going through this process. After the first change you have
current = Bob("Ben", null, null, "local_id")
next = Bob(null, 1, null, "local_id")
How is it supposed to know that you want next to apply the change to age but not name? If you're just updating based on nullability,
#mfulton has a good answer. Otherwise you need to provide the information yourself.
infix fun <T : Any> T.merge(mapping: KProperty1<T, *>.() -> Any?): T {
//data class always has primary constructor ---v
val constructor = this::class.primaryConstructor!!
//calculate the property order
val order = constructor.parameters.mapIndexed { index, it -> it.name to index }
.associate { it };
// merge properties
#Suppress("UNCHECKED_CAST")
val merged = (this::class as KClass<T>).declaredMemberProperties
.sortedWith(compareBy{ order[it.name]})
.map { it.mapping() }
.toTypedArray()
return constructor.call(*merged);
}
Edit
infix fun <T : Any> T.merge(right: T): T {
val left = this;
return left merge mapping# {
// v--- implement your own merge strategy
return#mapping this.get(left) ?: this.get(right);
};
}
Example
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
val result = withName merge withAge merge withRemoteId;

Kotlin smart cast the second value of a pair with filter

I'm trying to write a function that maps a String and Int? into a pair, then filters for non null second values in the pair, before continuing to map.
My code looks like this:
val ids: List<String> = listOf("a", "b", "c")
val ints: Map<String, Int?> = mapOf("a" to 1, "b" to 2, "c" to null)
ids.map { id: String ->
Pair(id, ints[id])
}.filter { pair -> pair.second != null}.map { pair: Pair<String, Int> ->
func(id, pair.second)
}
The problem is that the second map has the error:
Type inference failed: Cannot infer type parameter T in
inline fun <T, R> kotlin.collections.Iterable<T>.map ( transform (T) -> R ): kotlin.collections.List<R>
This looks like because the compiler does not know to smart cast my Iterable<Pair<String, Int?>> into an Iterable<Pair<String, Int>> after my filter. What can I do instead to solve this?
Kotlin's smart cast is usually not applicable outside method boundaries. However, there are a couple of ways you can achieve your goal anyway.
First, you can simply tell the compiler that the second value of the pair is never null by using the !! operator like so:
ids.map { id: String -> Pair(id, ints[id]) }
.filter { pair -> pair.second != null }
.map { pair: Pair<String, Int?> -> func(pair.second!!) }
Second, you can reverse the order of filter and map and apply the !! operator earlier:
ids.filter { id: String -> ints[id] != null }
.map { id: String -> id to ints[id]!! } //equivalent to Pair(id, ints[id]!!)
.map { pair: Pair<String, Int> -> func(pair.second) }
Finally, you can make it work without the !! operator by combining the filtering and the mapping in one step using the mapNotNull extension method:
ids.mapNotNull { id: String -> ints[id]?.let { id to it } }
.map { pair: Pair<String, Int> -> func(pair.second) }