Kotlin: filterNotNull for values in a Map<K,V?> - kotlin

Here's the Kotlin code I'd like to write (with extra type annotations for clarity):
fun test(alpha: String, beta: String, gamma: String? = null, delta: String? = null) {
val r1: Map<String, String?> =
hashMapOf(
"alpha" to alpha,
"beta" to beta,
"gamma" to gamma,
"delta" to delta
)
val r2: Map<String, String> = r1.filterValues { it != null }
callSomeFunction(r2) // expects a parameter of type Map<String, String>
}
Unfortunately, r1.filterValues { it != null } gives me back a Map<String, String?>, not a Map<String, String>. I understand why this is; it's the same reason that listOf(1, null).filter { it != null } has a different type from listOf(1, null).filterNotNull(). However, I still need to solve my problem!
Is there an idiomatic way to "filterValuesNotNull" from a Map?
Or, stepping up a level, is there some other idiomatic way to say "give me a Map of the following arguments, but skip those whose values are null"? I could resort to this, but I don't want to:
fun test(alpha: String, beta: String, gamma: String? = null, delta: String? = null) {
var r1: MutableMap<String, String> = mutableMapOf(
"alpha" to alpha,
"beta" to beta
)
if (gamma != null) {
r1["gamma"] = gamma
}
if (delta != null) {
r1["delta"] = delta
}
callSomeFunction(r1) // expects a parameter of type Map<String, String>
}

The functions that in my opinion should be in the stdlib:
fun <K,V: Any> Map<K,V?>.filterNotNullValuesTo(destination: MutableMap<K,V>): Map<K, V> {
for ((key, value) in entries) if (value != null) destination[key] = value
return destination
}
fun <K,V: Any> Map<K,V?>.filterNotNullValues(): Map<K,V> = filterNotNullValuesTo(mutableMapOf())

Here's 2 ways to do it, although whether either is idiomatic, I do not know.
#Suppress("UNCHECKED_CAST)
fun filterNotNullUnchecked(map: Map<String, String?>): Map<String, String> =
map.filterValues { it != null } as Map<String, String>
EDIT: As #Tenfour04 pointed out, this isn't unsafe, just unchecked because of type erasure, and adding a Suppress("UNCHECKED_CAST) annotation makes the warning go away.
fun filterNotNullUgly(map: Map<String, String?>): Map<String, String> {
val map2 = HashMap<String, String>()
for ((k, v) in map) if (v != null) map2[k] = v
return map2
}

hm, I suppose, it can be decided on that way
val newMap: Map<String, String> = r1.mapNotNull { (key, value) ->
value?.let { key to it }
}.toMap()

Related

how to elegantly create a map with only non-null values in Kotlin

How can I rewrite this code without having to resort to a MutableMap and conversion to immutable map?
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> {
val map = mutableMapOf("key1" to mandatoryValue)
optionalValue?.let { map.put("key2", it) }
return map.toMap()
}
My alternative solution isn't very nice either because it needs an unsafe cast:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> =
mapOf(
"key1" to mandatoryValue,
"key2" to optionalValue
).filterValues { it != null } as Map<String, String>
What I am looking for is something in the lines of:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> =
mapOf(
"key1" to mandatoryValue,
optionalValue?.let { "key2" to it }
)
You can have your own extension function as follows and then use it to filter null values from a Map:
fun <K, V> Map<K, V?>.filterValuesNotNull() =
mapNotNull { (k, v) -> v?.let { k to v } }.toMap()
toMap() does not necessarily create an immutable map. It is only guaranteed to be read-only. The underlying class instance might be a MutableMap (which in the current implementation is true if it has more than one key). Therefore, toMap() in your first block of code is unnecessary. The MutableMap is automatically upcast to Map when you return it since you specified Map as the return type. So, you could have put
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> {
val map = mutableMapOf("key1" to mandatoryValue)
optionalValue?.let { map.put("key2", it) }
return map
}
or
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> =
mutableMapOf("key1" to mandatoryValue).apply {
if (optionalValue != null) put("key2", optionalValue)
}
To get the syntax you requested in your last example, you could create an overload of mapOf that accepts and filters null values:
fun <K, V> mapOf(vararg pairs: Pair<K, V>?): Map<K, V> =
mapOf(*pairs.filterNotNull().toTypedArray())
There's nothing wrong with using MutableMap - in fact your first solution (without the redundant toMap()) is already pretty elegant. It's simpler and clearer than any immutable answer will be. Immutable operations come at the cost of additional object creations and copies, so unless you need the guarantees of immutability, it's best to use a MutableMap but only expose it via the Map interface, as you are already doing.
If you really wanted to do it immutably, you could do it like this:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> =
mapOf("key1" to mandatoryValue) +
(optionalValue?.let { mapOf("key2" to it) } ?: emptyMap())
Or equivalently if you prefer:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> =
mapOf("key1" to mandatoryValue) +
if (optionalValue != null) mapOf("key2" to optionalValue) else emptyMap()
Or:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> {
val mandatoryMap = mapOf("key1" to mandatoryValue)
return optionalValue?.let { mandatoryMap + ("key2" to optionalValue) } ?: mandatoryMap
}
Or:
fun createMap(mandatoryValue: String, optionalValue: String?): Map<String, String> {
val mandatoryMap = mapOf("key1" to mandatoryValue)
return if (optionalValue != null) mandatoryMap + ("key2" to optionalValue) else mandatoryMap
}
In kotlin 1.6
buildMap {
put("key1", mandatoryValue)
if (optionalValue != null)
put("key2", optionalValue)
}
This creates a mutableMap under the sheets, but is quite elegant and readable <3

Deep merging data classes in Kotlin

How can I do a recursive / deep merge of two data classes in Kotlin? Something like this:
import kotlin.reflect.*
import kotlin.reflect.full.*
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null
)
inline 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]!!
val type = property.returnType.classifier as KClass<*>
if (type.isData) {
parameter to this.merge(other) //inline function can't be recursive
} else {
parameter to (property.get(other) ?: property.get(this))
}
}
return primaryConstructor.callBy(args)
}
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"))
val u2 = User(age = 23, address = Address(zip = "33100"))
u1.merge(u2)
// expected: User(age = 23, name= "Tiina", address = Address(zip = "33100", street = "Hämeenkatu")
related: Combining/merging data classes in Kotlin
There were several problems in the posted code,
unnecessary reification and inlining
when type isData was detected instead of merging the values of the property merge on this with the other was called, so it became endless recursion.
get cannot be used on KProperty1<out T, Any?> because of the variance
some non-idiomatic stuff which works, but can be made better
Here's the fixed version. For production I would've added some checks and error messages, but this should work for "happy path" and hopefully give you the base to build on:
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null,
val map: Map<String, Int>? = null
)
fun <T> mergeData(property: KProperty1<out T, Any?>, left: T, right: T): Any? {
val leftValue = property.getter.call(left)
val rightValue = property.getter.call(right)
return rightValue?.let {
if ((property.returnType.classifier as KClass<*>).isSubclassOf(Map::class)) (leftValue as? Map<*, *>)?.plus(it as Map<*, *>)
else leftValue?.merge(it)
} ?: rightValue ?: leftValue
}
fun <T> lastNonNull(property: KProperty1<out T, Any?>, left: T, right: T) =
property.getter.call(right) ?: property.getter.call(left)
fun <T : Any> T.merge(other: T): T {
val nameToProperty = this::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = this::class.primaryConstructor!!
val args: Map<KParameter, Any?> = primaryConstructor.parameters.associateWith { parameter ->
val property = nameToProperty[parameter.name]!!
val type = property.returnType.classifier as KClass<*>
when {
type.isData || type.isSubclassOf(Map::class) -> mergeData(property, this, other)
else -> lastNonNull(property, this, other)
}
}
return primaryConstructor.callBy(args)
}
// verification
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"), map = mapOf("a" to 1))
val u2 = User(age = 23, address = Address(zip = "33100"), map = mapOf("b" to 2))
check(
u1.merge(u2) == User(
age = 23,
name = "Tiina",
address = Address(zip = "33100", street = "Hämeenkatu"),
map = mapOf("a" to 1,"b" to 2)
)
) {
"doesn't work"
}
println("Works!")

Dagger 2 Set Multibinding doesn't work for SimpleEntry in Kotlin?

The below multibinding works, when provide a Pair as IntoSet
#Provides
#IntoSet
fun entryOne(): Pair<String, String> {
val key = randomStringGenerator()
val value = "Random Value 1"
return Pair(key, value)
}
#Provides
#IntoSet
fun entryTwo(): Pair<String, String> {
val key = randomStringGenerator()
val value = "Random Value 2"
return Pair(key, value)
}
#Provides
fun randomKeyValueMap(entries: Set<Pair<String, String>>): Map<String, String> {
val randomKeyValueMap = LinkedHashMap<String, String>(entries.size)
for (entry in entries) {
randomKeyValueMap[entry.first] = entry.second
}
return randomKeyValueMap
}
However when turn Pair into SimpleEntry, it doesn't work anymore.
#Provides
#IntoSet
fun entryOne(): AbstractMap.SimpleEntry<String, String> {
val key = randomStringGenerator()
val value = "Random Value 1"
return AbstractMap.SimpleEntry(key, value)
}
#Provides
#IntoSet
fun entryTwo(): AbstractMap.SimpleEntry<String, String> {
val key = randomStringGenerator()
val value = "Random Value 2"
return AbstractMap.SimpleEntry(key, value)
}
#Provides
fun randomKeyValueMap(entries: Set<AbstractMap.SimpleEntry<String, String>>): Map<String, String> {
val randomKeyValueMap = LinkedHashMap<String, String>(entries.size)
for (entry in entries) {
randomKeyValueMap[entry.key] = entry.value
}
return randomKeyValueMap
}
It complaints
error: [Dagger/MissingBinding] java.util.Set<? extends java.util.AbstractMap.SimpleEntry<java.lang.String,java.lang.String>> cannot be provided without an #Provides-annotated method.
public abstract interface MyComponent {
^
java.util.Set<? extends java.util.AbstractMap.SimpleEntry<java.lang.String,java.lang.String>> is injected at
Note, if I use the Entry for Java, it works fine. Only doesn't work for Kotlin.
Looks like I need #JvmSuppressWildcards
#Provides
#IntoSet
fun entryOne(): Map.Entry<String, String> {
val key = randomStringGenerator()
val value = "Random Value 1"
return AbstractMap.SimpleEntry(key, value)
}
#Provides
#IntoSet
fun entryTwo(): Map.Entry<String, String> {
val key = randomStringGenerator()
val value = "Random Value 2"
return AbstractMap.SimpleEntry(key, value)
}
#Provides
#JvmSuppressWildcards
fun randomKeyValueMap(entries: Set<Map.Entry<String, String>>): Map<String, String> {
val randomKeyValueMap = LinkedHashMap<String, String>(entries.size)
for (entry in entries) {
randomKeyValueMap[entry.key] = entry.value
}
return randomKeyValueMap
}

Kotlinx Serialization - Custom serializer to ignore null value

Let's say I'm having a class like:
#Serializable
data class MyClass(
#SerialName("a") val a: String?,
#SerialName("b") val b: String
)
Assume the a is null and b's value is "b value", then Json.stringify(MyClass.serializer(), this) produces:
{ "a": null, "b": "b value" }
Basically if a is null, I wanted to get this:
{ "b": "b value" }
From some research I found this is currently not doable out of the box with Kotlinx Serialization so I was trying to build a custom serializer to explicitly ignore null value. I followed the guide from here but couldn't make a correct one.
Can someone please shed my some light? Thanks.
You can use explicitNulls = false
example:
#OptIn(ExperimentalSerializationApi::class)
val format = Json { explicitNulls = false }
#Serializable
data class Project(
val name: String,
val language: String,
val version: String? = "1.3.0",
val website: String?,
)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin", null, null)
val json = format.encodeToString(data)
println(json) // {"name":"kotlinx.serialization","language":"Kotlin"}
}
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#explicit-nulls
Use encodeDefaults = false property in JsonConfiguration and it won't serialize nulls (or other optional values)
Try this (not tested, just based on adapting the example):
#Serializable
data class MyClass(val a: String?, val b: String) {
#Serializer(forClass = MyClass::class)
companion object : KSerializer<MyClass> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
init {
addElement("a")
addElement("b")
}
}
override fun serialize(encoder: Encoder, obj: MyClass) {
encoder.beginStructure(descriptor).run {
obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
encodeStringElement(descriptor, 1, obj.b)
endStructure(descriptor)
}
}
override fun deserialize(decoder: Decoder): MyClass {
var a: String? = null
var b = ""
decoder.beginStructure(descriptor).run {
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> break#loop
0 -> a = decodeStringElement(descriptor, i)
1 -> b = decodeStringElement(descriptor, i)
else -> throw SerializationException("Unknown index $i")
}
}
endStructure(descriptor)
}
return MyClass(a, b)
}
}
}
Since I was also struggling with this one let me share with you the solution I found that is per property and does not require to create serializer for the whole class.
class ExcludeIfNullSerializer : KSerializer<String?> {
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString()
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("ExcludeNullString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
if (value != null) {
encoder.encodeString(value)
}
}
}
will work as expected with the following class
#Serializable
class TestDto(
#SerialName("someString")
val someString: String,
#SerialName("id")
#EncodeDefault(EncodeDefault.Mode.NEVER)
#Serializable(with = ExcludeIfNullSerializer::class)
val id: String? = null
)
Note the #EncodeDefault(EncodeDefault.Mode.NEVER) is crucial here in case you using JsonBuilder with encodeDefaults = true, as in this case the serialization library will still add the 'id' json key even if the value of id field is null unless using this annotation.
JsonConfiguration is deprecated in favor of Json {} builder since kotlinx.serialization 1.0.0-RC according to its changelog.
Now you have to code like this:
val json = Json { encodeDefaults = false }
val body = json.encodeToString(someSerializableObject)
As of now, for anyone seeing this pos today, default values are not serialized (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default)
So you simply add to set a default null value, and it will not be serialized.

What's the equivalent of `mapNotNull` that result in map?

I can convert a List<Int?> to List<Int> using mapNotNull function as shown below.
#Test
fun main() {
val testData = listOf(1, null, 3, null)
val noNull = processAwayNull(testData)
}
private fun processAwayNull(testData: List<Int?>): List<Int> {
return testData.mapNotNull{ it }
}
How could I convert Map<String, Int?> to Map<String, Int>?
The below with testData.filter { it.value != null } doesn't works, as it still produce Map<String, Int?>.
#Test
fun main() {
val testData = mapOf("One" to 1, "Two" to null, "Three" to 3, "Four" to null)
val noNull = processAwayNull(testData)
}
private fun processAwayNull(testData: Map<String, Int?>): Map<String, Int> {
return testData.filter { it.value != null }
}
Well, not really out of the box (in the sense that you get Map<String, Int> immediately), but what about filterValues?
testData.filterValues { it != null } // gives Map<String, Int?> but without null-values
Combining or replacing that with mapValues (maybe you can use a default value instead of null?):
// combining:
testData.filterValues { it != null }
.mapValues { (_, value) -> value as Int }
// replacing:
testData.mapValues { (_, value) -> value ?: /* default value */ 0 }
Both give a Map<String, Int> but the first creates and fills 2 maps under the hood and the second uses 0 instead of null.
You can also simplify the filterValues-variant with an appropriate unchecked cast, as "we know it better":
testData.filterValues { it != null } as Map<String, Int> // unchecked cast, because: we really do know better, do we? ;-)
Alternatively, you could also just handle all entries the way you knew already (using mapNotNull) and then create a new map out of it:
testData.asSequence()
.mapNotNull { (key, value) ->
value?.let {
key to it
}
}
.toMap() // giving Map<String, Int>
If you require that more often you may even want to have your own extension function in place:
#Suppress("UNCHECKED_CAST")
fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
Now you can use it similar as to follows:
testData.filterValuesNotNull() // giving Map<String, Int>
Possible alternative with custom helper function:
inline fun <K, V, R> Map<K, V>.mapValuesNotNullToMap(transformValue: (V) -> R?): Map<K, R> =
buildMap {
this#mapValuesNotNullToMap.entries.forEach { (key, value) ->
transformValue(value)?.let { put(key, it) }
}
}