Either & Ior (& Result)
Both Either<E, A>
and Ior<E, A>
hold values that may be of type E
or A
.
By convention, the type E
represents errors and the type A
represents
success. For example, Either<DbError, User>
could be a good result type for
a function that accesses a database and returns a User
but may also fail
with a DbError
.
Either<E, A>
only admits these two possibilities: a Left
holding a value of
type E
or a Right
holding a value of type A
. On the other hand, Ior<E, A>
provides a third option, namely Both
. Using Both
, you can represent states
that are considered successful but with some potential errors during execution;
like a compiler that finishes successfully but has some warnings. Nevertheless,
Ior
is not used very often.
These two types look very similar to the
Result
that comes with the standard library. However, Result
is tighly coupled to the
coroutine and exception machinery in the language. In fact, the "failure" case
only admits Throwable
errors. Even though the use case is more niche, the same
API is also available for Result
values.
Using builders
The preferred way to work with Either
and Ior
is to use builders.
Those start with a call to either
or ior
followed by a lambda; inside that
block, we can access the uniform typed errors API with functions like raise
,
ensure
, and recover
. For the Result
type the builder is called result
,
although in that case most errors come from
runCatching
.
import arrow.core.raise.either
import arrow.core.raise.ensure
data class MyError(val message: String)
fun isPositive(i: Int): Either<MyError, Int> = either {
ensure(i > 0) { MyError("$i is not positive") }
i
}
suspend fun example() {
isPositive(-1) shouldBe MyError("-1 is not positive").left()
isPositive(1) shouldBe 1.right()
}
To give you the complete picture, inside those blocks, the potential errors are
represented by a receiver of type Raise<E>
. Functions with that receiver can
be transformed into a variety of types; not only Either
and Ior
, but also
Result
, Option
, or a nullable type.
A common scenario is to have a potentially erroneous value (not only limited
to that of the block) that we want to unwrap.
That is, we want potential errors in those values to bubble
as errors of the entire block or keep the execution if the value represents
success. In those cases, we need to call .bind()
over that value.
We recommend using the custom NoEffectScopeBindableValueAsStatement
rule
for Detekt to prevent forgetting .bind()
inside
an either
or ior
block.
Combining Ior errors
The flow in an Either
block is simple: we execute each step; if at some point
we bind()
a Left
or find a raise
, we stop and return that value; if we get
to the end, we wrap the result in Right
. ior
blocks are a bit more complicated,
since we may end up in a situation in which we have errors to be reported, yet
we also have a value to continue the execution. This brings up a question: what
should we do if several steps in the block are Both
? The current API leaves
the answer open to the developer. The ior
builder has
an additional parameter that specifies how to combine several errors.
Without builders
In some scenarios, builders may be overkill for the task at hand. For those cases,
we provide functions that create or operate directly on Either
and Ior
.
On the generation front, extension functions like .left()
and .right()
provide another way to write expressions that won't obscure the inner contents
as much as a constructor. Validations are often written in that style.
// this is the type we want to construct
@JvmInline value class Age(val age: Int)
// these are the potential problems
sealed interface AgeProblem {
object InvalidAge: AgeProblem
object NotLegalAdult: AgeProblem
}
// validation returns either problems or the constructed value
fun validAdult(age: Int): Either<AgeProblem, Age> = when {
age < 0 -> AgeProblem.InvalidAge.left()
age < 18 -> AgeProblem.NotLegalAdult.left()
else -> Age(age).right()
}
Another way to obtain an Either
is using Either.catch
, which wraps a
computation that may throw exceptions and returns a Left
if that's the case.
Essentially, runCatching
from the standard library,
but replacing Result
with Either
.
The rest of the API closely follows the one from typed errors.
For example, you can call recover
or zipOrAccumulate
directly on Either
without the need for an additional either { }
block. One potentially useful
function not part of builders is mapLeft
,
which applies a function when the value represents an error. This scenario often
arises when your code has a hierarchy of different error types.
Either for validation
Either
has a double face: it can be used to model problems in a piece of code,
pretty much like exceptions, but also to define validations over some input data.
The difference between these two scenarios is how we react to several problems
arising in a piece of code.
- When we think of exceptions, we have a fail-fast or fail-first approach: Once we discover a problem, we want to stop execution and immediately report to the caller. In those scenarios, steps depend on one another, so it makes no sense to keep trying.
- When we think of validation, we want to be as comprehensive as possible with potential problems with the input data. In other words, if the given name and age are wrong, we want to report both, not just the first one. This approach is called accumulation and arises when the code is computations whose failure is independent of each other.
By default, an either
block follows the first approach. If you want to accumulate
errors instead, you should use zipOrAccumulate
,
or mapOrAccumulate
.
The difference is that the former takes the different computations as arguments,
and they can return different types, whereas the latter applies the same computation
uniformly to elements of an Iterable
.
One common pattern when describing validations is to have an Either
with
List<Problem>
as the error type. Arrow provides a more refined version
where we ensure that we never end up in an awkward situation in which we have
a Left
value, but the list of problems is empty.
public typealias EitherNel<E, A> = Either<NonEmptyList<E>, A>
In Arrow 1.x series, a different type called Validation
embodied
the accumulation strategy for errors. However, the API was almost identical, and
sometimes code became flooded with conversion back and forth between Either
and Validation
.
Arrow 2.x provides a single Either
type instead, but we encourage you to use
the EitherNel
type alias if you are describing a validation.