neu242
neu242

Reputation: 16575

Filter null keys and values from map in Kotlin

I have an extension function that filters away entries with null keys or values:

fun <K, V> Map<K?, V?>.filterNotNull(): Map<K, V> = this.mapNotNull { 
   it.key?.let { key -> 
      it.value?.let { value -> 
         key to value 
      }
   }
}.toMap()

This works for a map with nullable keys and values:

data class Stuff(val foo: Int)

val nullMap = mapOf<String?, Stuff?>(null to (Stuff(1)), "b" to null, "c" to Stuff(3))
assert(nullMap.filterNotNull().map { it.value.foo } == listOf(3))

But not in one that has non-nullable keys or values:

val nullValues = mapOf<String, Stuff?>("a" to null, "b" to Stuff(3))    
assert(nullValues.filterNotNull().map { it.value.foo } == listOf(3))

Type mismatch: inferred type is Map<String, Stuff?> but Map<String?, Stuff?> was expected
Type inference failed. Please try to specify type arguments explicitly.

Is there a way to make my extension function work for both cases, or do I need to provide two separate functions?

Upvotes: 4

Views: 9871

Answers (4)

goerge
goerge

Reputation: 21

As improvement to the nested lets, you can use:

private fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
    mapNotNull { (k, v) ->
        when {
            k == null -> null
            v == null -> null
            else -> k to v
        }
    }.toMap()

which can be condensed to

private fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
    mapNotNull { (k, v) -> if (k == null || v == null) null else k to v }.toMap()

Upvotes: 2

goedi
goedi

Reputation: 2083

You can specify the Map type when using mapOf

assert(mapOf<String?, String?>("x" to null, "a" to "b").filterNotNull() == mapOf("a" to "b"))

EDIT

You specified the extension function for Map<K?, V?> and was trying to use it an inferred Map<String, String> (in your original question), so it wouldn't work as Map<String, String> is not a subtype of Map<K?, V?>, cause map interface is defined as Map<K, out V>. It is invariant on the key parameter type and covariant on the value parameter type.

What you can do is to make the key type in your extension function also covariant by changing Map<K?, V?> to Map<out K?, V?> instead. Now, Map<String, String> or Map<String, String?> will be a subtype of Map<K? V?>.

You can also use a biLet instead two nested let: How can I check 2 conditions using let (or apply etc)

Upvotes: 2

Frank Neblung
Frank Neblung

Reputation: 3165

The solution

fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> = this.mapNotNull {
    it.key?.let { key ->
        it.value?.let { value ->
            key to value
        }
    }
}.toMap()

seems overcomplicated to me. It could be written as

fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
    filter { it.key != null && it.value != null } as Map<K, V>

The ugly cast is necessary, since the compiler can not (yet) deduce, that neither keys nor values contain null.


A word of warning, concerning the covariance out K?

Yes, it offers the possibility to use the same method not only for Map<String?, Stuff?> but also for Map<String, Stuff?> (key nullable or not). But this freedom comes with a cost. For maps, known to have no null keys, you needlessly pay the null comparison for every entry.

In your initial solution - that without covariance on K -, the compiler could prevent you from calling that inefficient method. The proper method then probably is

fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>

Upvotes: 3

tieskedh
tieskedh

Reputation: 337

I will later figure out why, but adding out to the map is working:

fun <K : Any, V : Any> Map<out K?, V?>.filterNotNull(): Map<K, V> = ...

Upvotes: 7

Related Questions