3

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?

neu242
  • 15,796
  • 20
  • 79
  • 114

3 Answers3

7

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> = ...
tieskedh
  • 327
  • 5
  • 10
3

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>
Frank Neblung
  • 3,047
  • 17
  • 34
  • 1
    Thanks! I intentionally asked for a function covering *both* null keys and values, so that latter comment is quite useless :) Also, I try to avoid explicit (and ugly) casts (as well as abdominations like `!!`), so using an explicitly safe call like `?.let` is definitely the way to go for me. – neu242 Nov 18 '19 at 08:17
1

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)

Diego Marin Santos
  • 1,923
  • 2
  • 15
  • 29