The Either Type
In the previous article, we discussed functional error handling using Try, I also mentioned the existence of another similar type called Either
.
From VAVR docs:
Either
represents a value of two possible types. AnEither
is either aLeft
or aRight
.
What on the green earth is that?
Like Try
, Either
is a container type. Unlike the aforementioned one, it takes not only one, but two type parameters:
an Either<A, B>
instance can contain either an instance of A
, or an instance of B
. This is different from a Tuple, which contains both
an A
and a B
instance.
By convention, a Left value signifies a failure case result and a Right value signifies a success. You can think of a (say) Try<String>
as an Either<Throwable, String>
.
How to create an Either
// Remember this?
Alcoholic buyAlcoholic(Customer customer) throws UnderageException {
if (customer.getAge() < 18) {
throw new UnderageException("Age: " + customer.getAge());
}
return new Alcoholic(5);
}
// From boring to fun!
public class UnderageError {
private final String reason;
public String getReason() {
return reason;
}
}
Either<UnderageError, Alcoholic> buyAlcoholicEither(Customer customer) {
if (customer.getAge() < 18) {
return Either.left(new UnderageError("Age: " + customer.getAge()));
}
return Either.right(new Alcoholic(5));
}
If we call buyAlcoholicEither(new Customer(16))
we will get an UnderageError
wrapped in a Left. If we pass in new Customer(20)
, the return value will be a Right containing a Alcoholic
.
How to use an Either
You can know if an Either isLeft
or isRight
. You can also do pattern matching on it (which is the best way of working with objects of this type).
Either<UnderageError, Alcoholic> alcoholicEither = //...
Match(alcoholicEither).of(
Case($Right($()), c -> "drinked " + c),
Case($Left($()), t -> "troubles")
);
Once you have an Either
, you can call map
on it:
Either<UnderageError,Integer> concentrationOrError = alcoholicEither.map(Alcoholic::getAlcoholConcentration);
If it’s called on a Right
, the value inside it will be transformed. If it’s a Left
, that will be returned unchanged.
We can also change the Right success convention using projections:
If the given Either is a Right and projected to a Left, the Left operations have no effect on the Right value. If the given Either is a Left and projected to a Right, the Right operations have no effect on the Left value. If a Left is projected to a Left or a Right is projected to a Right, the operations have an effect.
Either<String, Alcoholic> concentrationOrError = alcoholicEither.left().map(UnderageError::getReason).toEither();
Other Features
We can fold Left and Right to one common type:
String reasonOrAlcoholic = alcoholicEither.fold(UnderageError::getReason, Alcoholic::toString);
or swap sides:
Either<Alcoholic, UnderageError> swap = alcoholicEither.swap();
Either
supports all the higher-order methods: map
( and mapLeft
), flatMap
, filter
, forEach
, …
Either as an Exception replacement
One of the nice features of Java exceptions is the ability to declare several different potential exception types as part of a method signature. Either can do that as well! And without the exception goto style handling and performance penalties:
public interface Error {
String getReason();
}
public class UnderageError implements Error {
private final String reason;
@Override public String getReason() {
return reason;
}
}
public class ImpossibleError implements Error {
@Override public String getReason() {
return "Black magic happened";
}
}
Either<Error, Alcoholic> alcoholicEither = //...
Match(alcoholicEither).of(
Case($Right($()), c -> "drinked " + c),
Case($Left($(instanceOf(UnderageError.class))), t -> "troubles"),
Case($Left($(instanceOf(ImpossibleError.class))), t -> "wizard")
);
Nice and smooth as silk
Conclusions and thoughts
When you learn a new paradigm, you need to reconsider all the familiar ways of solving problems. Functional programming uses different idioms to report error conditions!
Either
is a type that is not without flaws, and whether you want to have to deal with them and incorporate it in your own code is ultimately up to you.
Memento: throwing an exception will break your functional composition and probably result in unexpected behaviour for the caller of your function. So it should be reserved as a method of last resort, for when the other options don’t make sense.
Comments