I have came a cross a need to combine multiple Arrow-kt eithers: Given the following
typealias ApiResult<T> = Either<Exception, T>
interface ApiClient {
fun getUserIds(): ApiResult<List<String>>
fun getUserName(id: String): ApiResult<String>
fun getUserAge(id: String): ApiResult<Int>
fun getUserEmail(id: String): ApiResult<String>
}
data class UserData(val name: String, val age: Int, val email: String)
fun collectSomeHow(
name: ApiResult<String>,
age: ApiResult<Int>,
email: ApiResult<String>
):ApiResult<UserData> = TODO()
val apiClient:ApiClient = getApiClient()
val result: ApiResult<List<ApiResult<UserData>>> = apiClient.getUserIds().map{ ids->
ids.map{ id->
val name = apiClient.getUserName(id)
val age = apiClient.getUserAge(id)
val email = apiClient.getUserEmail(id)
collectSomeHow(name, age, email)
}
}
How could collectSomeHow
be implemented to return ApiResult<UserData>
?
You can use zipOrAccumulate
to combine multiple Eithers:
fun collectSomeHow(
name: ApiResult<String>,
age: ApiResult<Int>,
email: ApiResult<String>,
): ApiResult<UserData> = Either
.zipOrAccumulate(name, age, email, ::UserData)
.mapLeft { it.first() }
The Right is what you specify in the transform lambda (the last parameter, here for brevity's sake the function reference ::UserData
). The Left is a list of all the Exceptions that might have occurred (a maximum of three since each of name
, age
and email
may be Left).
Since the final return type should be an Either that has only a single Exception for Left I used mapLeft
to extract only the first exception that occurred from the list of exceptions, the remaining ones will be swallowed. Adapt this as you see fit.
If you need more flexibility you can also use the Raise DSL available in the either
builder:
fun collectSomeHow(
name: ApiResult<String>,
age: ApiResult<Int>,
email: ApiResult<String>,
): ApiResult<UserData> = either {
UserData(
name = name.bind(),
age = age.bind(),
email = email.bind(),
)
}
bind
unwraps the original Eithers, raising any potential errors in the order they appear. This also prevents the execution of the remaining code, making it potentially more efficient because of its fail-fast behavior.
The result is a new Either with the newly created UserData
object as the Right value or the first error that was raised as the Left value.
This approach will generally feel more natural to use, although it is more verbose in this specific case since you cannot use a function reference anymore.