23

I'm trying to think of a function that would allow a Map<String, Any?> object to be treated as Map<String,Any> through type inference through applying a single function.

I am pretty new to the transformation functions in Kotlin and have tried the various filter and filterValues filterNot on the map like so:

val input = mapOf(Pair("first",null))
val filtered: Map<String,Any> = input.filter { it.value!=null }

it also fails to compile with any of these

input.filterValues { it!=null }
input.filterNot { it.value==null }
input.filterNot { it.value is Nothing }

The closest I can seem to get is applying multiple steps or having an Unchecked cast warning. I would have thought that filtering the values to be !=null would suffice. My only other thought is that it's due to the generics?

Harry J
  • 390
  • 1
  • 3
  • 11

3 Answers3

25

The filter functions return a Map with the same generic types as the original map. To transform the type of the value, you need to map the values from Any? to Any, by doing a cast. The compiler can't know that the predicate you pass to filter() makes sure all the values of the filtered map are non-null, so it can't use type inference. So your best et is to use

val filtered: Map<String, Any> = map.filterValues { it != null }.mapValues { it -> it.value as Any }

or to define a function doing the filtering and the transformation in a single pass, and thus be able to use smart casts:

fun filterNotNullValues(map: Map<String, Any?>): Map<String, Any> {
    val result = LinkedHashMap<String, Any>()
    for ((key, value) in map) {
        if (value != null) result[key] = value
    }
    return result
}
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • can the call to `mapValues` be simplified or are we stuck with taking in the `Map.Entry`? It seems weird that it wouldn't just provide the value of type V and expect a return of type R to me but I suppose that would incur a performance cost that iterating over an entry set doesn't. Maybe the function name is too ambiguous? – Harry J Feb 18 '17 at 09:49
  • I just realised that `.mapValues { it.value as Any }` achieves the same result, and removes passing the already accessible `it` – Harry J Feb 18 '17 at 10:00
  • 1
    I don't think it has anything to do with performance. Whether the mapper function takes an entry or a value doesn't change anything: the mapValues() function still needs to iterate over all entries. Taking an entry rather than just a value allows doing more things than just taking the value, because you might need to have access to the key to decide how to transform the value. – JB Nizet Feb 18 '17 at 10:02
  • yeah that's a good point. I actually decided this implementation was better from removing the Unchecked cast warning, and ended up just writing an extension function `fun Map.filterNullValues() = this.filterValues{it!=null}.mapValues { it.value as V }` which seems to pass a trivial test I checked against. – Harry J Feb 18 '17 at 10:09
  • 1
    It's less efficient than removing the warning, and removing the warning is safe in this case. If you really create your own function anyway, implementing it as I show in my answer would be more efficient than doing two passes on the map. – JB Nizet Feb 18 '17 at 10:10
  • Right. That makes sense from the performance perspective. Kotlin's stream/iterate-like functions are still a bit lost on me being so new. – Harry J Feb 18 '17 at 10:14
  • why don't you use extension? – 최봉재 Sep 28 '20 at 06:48
16

The compiler just doesn't perform type analysis deep enough to infer that, for example, input.filterValues { it != null } filters out null values from the map and thus the resulting map should have a not-null value type. Basically there can be arbitrary predicate with arbitrary meaning in terms of types and nullability.

There is no special case function for filtering null values out of a map in the stdlib (like there is .filterIsInstance<T>() for iterables). Therefore your easiest solution is to apply an unchecked cast thus telling the compiler that you are sure about the type safety not being violated:

@Suppress("UNCHECKED_CAST")
fun <K, V> Map<K, V?>.filterNotNullValues() = filterValues { it != null } as Map<K, V>

See also: another question with a similar problem about is-check.

Community
  • 1
  • 1
hotkey
  • 140,743
  • 39
  • 371
  • 326
  • 1
    I like this function, it seems like it should be alongside the `filterNotNull()` for list / iterables though I'm not sure of how that would modify computation cost to apply the function in comparison to those – Harry J Feb 18 '17 at 09:19
2

This yields no warnings kotlin 1.5.30

listOfNotNull(
        nullableString?.let { "key1" to it },
        nullableString?.let { "key2" to it }
    ).toMap()
Matt Broekhuis
  • 1,985
  • 2
  • 26
  • 35
  • It yields a warning from me, though: This creates a ton of new objects you don't need. The solution by @hotkey is much nicer and way more efficient. – Jorn Aug 03 '23 at 07:18