How to use Either monad and avoid nested flatMap

1k views Asked by At

I'm in a situation where I'm trying to setup some data and then call a service. Each step can fail, so I'm trying to use Arrow's Either to manage this.

But I'm ending up with a lot of nested flatMaps.

The following code snippet illustrates what I'm trying to do:

import arrow.core.Either
import arrow.core.flatMap

typealias ErrorResponse = String
typealias SuccessResponse = String

data class Foo(val userId: Int, val orderId: Int, val otherField: String)
data class User(val userId: Int, val username: String)
data class Order(val orderId: Int, val otherField: String)

interface MyService {
    fun doSomething(foo: Foo, user: User, order: Order): Either<ErrorResponse, SuccessResponse> {
        return Either.Right("ok")
    }
}

fun parseJson(raw: String): Either<ErrorResponse, Foo> = TODO()
fun lookupUser(userId: Int): Either<ErrorResponse, User> = TODO()
fun lookupOrder(orderId: Int): Either<ErrorResponse, Order> = TODO()

fun start(rawData: String, myService: MyService): Either<ErrorResponse, SuccessResponse> {
    val foo = parseJson(rawData)
    val user = foo.flatMap {
        lookupUser(it.userId)
    }
    //I want to lookupOrder only when foo and lookupUser are successful
    val order = user.flatMap {
        foo.flatMap { lookupOrder(it.orderId) }
    }
    //Only when all 3 are successful, call the service
    return foo.flatMap { f ->
        user.flatMap { u ->
            order.flatMap { o ->
                myService.doSomething(f, u, o)
            }
        }
    }
}

I'm sure there is a better way to do this. Can someone help me with an idiomatic approach?

1

There are 1 answers

2
nomisRev On BEST ANSWER

You can use the either { } DSL, this is available in a suspend manner or in a non-suspend manner through the either.eager { } builder.

That way you can use suspend fun <E, A> Either<E, A>.bind(): A.

Rewriting your code example:

fun start(rawData: String, myService: MyService): Either<ErrorResponse, SuccessResponse> =
  either.eager {
    val foo = parseJson(rawData).bind()
    val user =  lookupUser(foo.userId).bind()
    val order = lookupOrder(foo.orderId).bind()
    myService.doSomething(foo, user, order).bind()
  }

If you run into an Either.Left, then bind() will short-circuit the either.eager block and return with the encountered Either.Left value.