Map<K, V> to Map<K, V!> (filterNotNullValues)

I want a conditional element in my map, but there’s no std function to convert a Map<*, T> to Map<*, T!!>
What I have:

val foo: Map<String, String> = mapOf(
  "foo" to "bar",
  "baz" to if (condition) "bat" else null,
).filterValues { it != null } as Map<String, String> // unsafe cast

What I want:

val foo: Map<String, String> = mapOf(
  "foo" to "bar",
  "baz" to if (condition) "bat" else null,
).filterNotNullValues()

I could use buildMap, but it’s clunky and experimental:

val foo: Map<String, String> = buildMap<String, String>(
  set("foo", "bar")
  if (condition) set("baz", "bat)
)

BONUS ROUND!!

Dart has conditional elements, which is what would be perfect here, something like:

val foo: Map<String, String> = mapOf(
  "foo" to "bar",
  if (condition) "baz" to "bat",
)

Off the top of my head without actually compiling it

val foo: Map<String, String> = sequence {
  yield "foo" to "bar"
  if (condition) yield "baz" to "bat"
).toMap()

but not sure that is any better.

1 Like

It looks almost exactly like what @kotlinIsland requested in the Bonus Round section.
Although we could take it further and create a filterValuesNotNull() function:

fun main() {
    val foo: Map<String, String?> = mapOf(
        "foo" to "bar",
        "baz" to null,
        "one" to null,
        "two" to "2")
    
    val fooNoNulls: Map<String, String> = foo.filterValuesNotNull() // <-- Here's the function
    println(fooNoNulls)
}

fun <T, R> Map<T, R?>.filterValuesNotNull(): Map<T, R> = entries
    .asSequence()
    .map {
        val v = it.value // We must capture the value of the map entry to trust our null check.
        if (v != null) it.key to v else null 
    }
    .filterNotNull()
    .toMap()

// Here's the sequence version:
/* fun <T, R> Map<T, R?>.filterValuesNotNull(): Map<T, R> = sequence<Pair<T, R>> {
    for ((key, value) in entries) {
        if (value != null) yield (key to value)
    }
}.toMap() */

EDIT:
This is the narrower case of transforming, Map.Entry<T, R?> to Map.Entry<T, R>. The implementation I came up with does that by “dropping” keys by mapping them to a null entry and filtering them out.

The main trouble with doing a null conversion is that you can’t map the values to non-nullable unless you capture and bind them as a new pair, otherwise they could still be null after (and during) the conditional check. We must make a new map and we can’t go by the assumption that the values happened to be null when we checked because they don’t promise to always be not-null.

You can either do what I did and assign a new val to capture the entry value, or use destructuring like the sequence version in the for loop.
Here’s the functional style version using destructuring:

fun <T, R> Map<T, R?>.filterValuesNotNull(): Map<T, R> = entries
    .asSequence() // Using sequence to avoid the extra collection allocation. It's optional.
    .map { (k, v) -> // Captured both the key and value as new variables, "k" and "v"
        if (v != null) k to v else null 
    }
    .filterNotNull()
    .toMap()
2 Likes