2
\$\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

1
\$\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\$
    – t3chb0t
    Jan 16 at 16:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.