m.reiter
m.reiter

Reputation: 2525

Filter Kotlin collection of pairs so pair content is not null

I am looking for a way to filter a List<Pair<T?, U?>> to a List<Pair<T, U>>: I would like to ignore pairs containing null.

Works, but i need smartcasting, too:

fun <T, U> List<Pair<T?, U?>>.filterAnyNulls() = 
    filter { it.first != null && it.second != null }

Works, but ugly !!, and does not feel idiomatic at all

fun <T, U> List<Pair<T?, U?>>.filterAnyNulls() = mapNotNull {
    if (it.first == null || it.second == null) {
        null
    } else {
        Pair(it.first!!, it.second!!)
    }
}

Any nice solutions? And any more generic solutions? (Maybe even in classes which can be deconstructed, like triples or data classes?)

Upvotes: 4

Views: 3724

Answers (4)

Konstantin Raspopov
Konstantin Raspopov

Reputation: 1615

This solution utilizes destructuring and smart casts and will do what you need without ugly !!:

fun <T, U> List<Pair<T?, U?>>.filterPairs(): List<Pair<T, U>> =
    mapNotNull { (t, u) ->
        if (t == null || u == null) null else t to u
    }

Upvotes: 5

Adam Millerchip
Adam Millerchip

Reputation: 23091

This is just a variation on your second suggestion really, but another option is to explicitly return the non-null types:

fun <T, U> List<Pair<T?, U?>>.filterAnyNulls(): List<Pair<T, U>> = this
    .filter { (a, b) -> a != null && b != null }
    .map { (a, b) -> a!! to b!! }

I was hoping that contracts might be the solution to avoid the !! shouting in the map, but it appears that they are still quite limited and nowhere near developed enough yet. I don't think there's a way to avoid the !! for now, at least until the compiler or contracts get smarter.

Upvotes: 1

gidds
gidds

Reputation: 18557

I don't think there's a good answer to this, unfortunately.

As you say, the filter() option gives a list of Pair<T?, U?> where the compiler still thinks the pair's values can be null even though we know they can't.

And the mapNotNull() option fixes that, but needs new Pair objects to be created, which is inefficient.  (The !! there are ugly, but I think this is one of the times when they're justified.  It's a shame that the compiler's smart casts aren't smart enough, but I understand that total type inference is an intractable problem, and it has to stop somewhere.)

I found a third option, which fixes both of those problems, but at the cost of an unsafe cast:

fun <T, U> List<Pair<T?, U?>>.filterNotNull()
    = mapNotNull{ it.takeIf{ it.first != null && it.second != null } as? Pair<T, U> }

Like the second option, this uses mapNotNull() to both filter and convert the pairs; however, it does a simple cast to tell the compiler that the type parameters are non-nullable.  This returns a Pair<T, U> so that the compiler knows the pair values cannot be null, and it avoids creating any new objects (apart from the overall list, of course).

However, it gives a compile-time warning about an unsafe cast.  It runs fine for me (Kotlin/JVM), but like all unsafe casts there's no guarantee it will work on all platforms or all future versions.  So it's not ideal.

All three options have drawbacks.  Perhaps the best overall is your second option; the code itself is a little ugly, and inefficient (creating new Pairs), but it's safe, will always work, and is nice to use.

(By the way, I think filterNotNull() would be a better name for this function.  In the standard library, filter functions seem to be named for what they keep, not what they drop, so filterAnyNulls() would give completely the wrong impression!)

Upvotes: 3

marstran
marstran

Reputation: 27971

You can first create a function the convert Pair<T?, U?> to Pair<T, U>?. I need to put first and second into some variables to make the smart casting work. You could use first?.let { t -> as well.

Something like this:

fun <T, U> Pair<T?, U?>.toNullablePair() {
    val t = first
    val u = second
    return if (t == null || u == null) null else t to u
}

Then you can create your filter-function using it:

fun <T, U> List<Pair<T?, U?>>.filterAnyNulls() = 
    mapNotNull { it.toNullablePair() }

Upvotes: 1

Related Questions