4
\$\begingroup\$

I'm trying to integrate Arrow-kt's Either into an application that needs to parse and validate filter input. This input is a comma separated list of criteria. Either sound like a good candidate for that job because it does both jobs at the same time.

Big-Picture

I've got a parser that takes care of the first step:

interface Parse<out T> {
    operator fun invoke(value: String): Either<String, T>
}

object ParseList : Parse<List<String>> {
    override fun invoke(value: String): Either<String, List<String>> {
        val values =
            value
                .split(",")
                .map { it.trim() }
                .filter { it.isNotEmpty() }
        return if (values.any()) Either.Right(values)
        else Either.Left("Value is not a list.")
    }
}

and other parsers that take care of an individual value like a date:

class ParseDate : Parse<LocalDate> {
    @Suppress("MemberVisibilityCanBePrivate")
    var formatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
    override fun invoke(value: String) =
        runCatching { formatter.parse(value) }
            .map { LocalDate.from(it) }
            .map { Either.Right(it) }
            .getOrElse { Either.Left("Value is not a date.") }
}

Combining these to components results in:

List<Either<String, T>> 

so it needs to be converted into this:

Either<String, List<T>> 

API

In orther to convert a List of Eithers into an Either of Lists i wrote the merge extension that processes each item and stops at the first Left, but keeps running on Right and collecting their values into a new list that is then return as a new Either:

fun <A, B> List<Either<A, B>>.merge(): Either<A, List<B>> {
    val result = mutableListOf<B>()
    for (x in this) {
        when (x) {
            is Either.Right -> result.add(x.value)
            is Either.Left -> return Either.Left(x.value)
        }
    }
    return Either.Right(result)
}

class MergeTest : FunSpec({
    test("Can stop on left.") {
        val list = listOf(
            "foo".right(),
            "bar".left(),
        )
        list.merge() shouldBe "bar".left()
    }
    test("Can merge right.") {
        val list = listOf(
            "foo".right(),
            "bar".right(),
        )
        list.merge() shouldBe listOf("foo", "bar").right()
    }
})

Is it necessary to do this on your own or is there a more clever way?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I think there is not simple built-in way to do this.

That said, your approach can be simplified a little bit:

fun <A, B> List<Either<A, B>>.merge(): Either<A, List<B>> =
    Either.Right(map {
        when (it) {
            is Either.Right -> it.value
            is Either.Left -> return it
        }
    })

It can be even reduced to a single line:

fun <A, B> List<Either<A, B>>.merge(): Either<A, List<B>> =
    Either.Right(map { (it as? Either.Right)?.value ?: return it as Either.Left })

Although this one is significantly less readable.

\$\endgroup\$
1
  • \$\begingroup\$ The usage of the return inside the map with the implicit outer return is crazy! I love it. I didn't know this was possible. \$\endgroup\$ Commented Jan 16, 2024 at 16:51

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.