Yuri Geinish
Yuri Geinish

Reputation: 17214

Find-first-and-transform for Sequence in Kotlin

I often stumble upon this problem but don't see a common implementation: how do I idiomatically (functionally) find an element, stop search after the match, and also return a different type (i.e. map whatever matched to another type)?

I've been able to do a workaround with

fun <F,T> Sequence<F>.mapFirst(block: (F) -> T?): T? =
    fold(AtomicReference<T>()) { ref, from ->
        if (ref.get() != null) return@fold ref
        ref.set(block(from))
        ref
    }.get()

fun main() {
    Files.list(someDir).asSequence().map { it.toFile() }.mapFirst { file ->
        file.useLines { lines ->
            lines.mapFirst { line ->
                if (line == "123") line.toInt() else null
            }
        }
    }?.let { num ->
        println("num is $num") // will print 123 as an Int
    } ?: println("not a single file had a line eq to '123'")
}

But that doesn't stop on the match (when block() returns non-null) and goes to consume all files and all their lines.

Upvotes: 4

Views: 1102

Answers (3)

Stephen Talley
Stephen Talley

Reputation: 1202

Kotlin has a firstNotNullOfOrNull function that does exactly this. It is a direct replacement for asSequence().mapNotNull { ... }.firstOrNull().

There is also a firstNotNullOf variant that throws a NoSuchElementException if no non-null value is produced.

Upvotes: 1

Willi Mentzel
Willi Mentzel

Reputation: 29844

I would not map the values you discard then, instead do it like this:

sequenceOf(1, 2, 3)
    .firstOrNull() { it == 2 }
    ?.let { it * 2 } ?: 6

First you find the value that matches your condition, then you transform it too whatever you want. In case you don't find a matching element, you assign a default value (in this case 6).

Upvotes: 0

Ilya
Ilya

Reputation: 23125

A simple for loop is enough to implement mapFirst:

fun <F,T> Sequence<F>.mapFirst(block: (F) -> T?): T? {
    for (e in this) { 
        block(e)?.let { return it }
    }
    return null
}

If you need a solution without introducing your own extensions (though there's nothing wrong with it), you can use mapNotNull + firstOrNull combination:

files.asSequence()
    .mapNotNull { /* read the first line and return not null if it's ok */ }
    .firstOrNull()

Upvotes: 2

Related Questions