Either
Either
is type that represents the existence of one of two types. An instance
of Either
is an instance of Left
or Right
.
Motivation
While Option
is a great weapon to have in our functional toolbelt, sometimes
it isn't enough. Sometimes it makes sense to provide additional information
when our functions don't follow the "happy path". For Option
s, the happy
path can be considered returning Some(...)
while the failure path would be
returning None
. But consider the example below:
final class User {
final String name;
final String alias;
final int age;
const User(this.name, this.alias, this.age);
}
Option<User> userOption(String name, String alias, int age) => (
Option.when(() => name.isEmpty, () => name),
Option.when(() => alias.isEmpty, () => alias),
Option.when(() => age >= 18, () => age),
).mapN(User.new);
Reading the code, we can see that a new user requires a non-empty name and a non-empty alias. Let's see what happens when we try to create a few users:
final create1 = userOption('Jonathan', 'Jon', 21); // Some(User(...))
final create2 = userOption('Jonathan', '', 32); // None()
final create3 = userOption('', 'Jon', 55); // None()
We can see that the function works as intended which is great but consider
the results returned in the failure case when the user's name and/or alias is
empty. They're all None
. It would be much better if we could return a reason
why the user couldn't be created right? So let's do better using the Either
type:
Either<String, User> userEither(String name, String alias, int age) {
if (name.isNotEmpty) {
if (alias.isNotEmpty) {
return Right(User(name, alias, age));
} else {
return const Left('Alias is required!');
}
} else {
return const Left('Name is required!');
}
}
final create4 = userEither('Jonathan', 'Jon', 21); // Right(Instance of 'User')
final create5 = userEither('Jonathan', '', 32); // Left(Alias is required!)
final create6 = userEither('', 'Jon', 55); // Left(Name is required!)
Much better! We can now see why the function was unable to create the user in each instance. This would be great information to pass along to the user to help them navigate our application.
We said earlier that the "happy" path for Option
is Some
vs. the failure
path of None
. So what is the happy/failure paths for Either
? Looking at
the previous examples, it should become clear that the happy path is Right
while the failure path is Left
. This is by convention so you could choose
to ignore this, but be aware that many combinators in the Either API treat
the Right side as the happy path, leading to the statement that Either
is
"right biased".
Combinators
map
Much like Option
the map
method on Either
will apply a function to
the value, so long as the Either
is an instance of Right
:
const myLeft = Left<int, String>(42);
const myRight = Right<int, String>('World');
String greet(String str) => 'Hello $str!';
final myLeft2 = myLeft.map(greet); // Left(42)
final myRight2 = myRight.map(greet); // Right(Hello World!)
flatMap
Chaining functions that return Either
is simple using the Either.flatMap
function:
Either<String, User> validateName(User u) =>
Either.cond(() => u.name.isNotEmpty, () => u, () => 'User name is empty!');
Either<String, User> validateAlias(User u) => Either.cond(
() => u.alias.isNotEmpty, () => u, () => 'User alias is empty!');
Either<String, User> validateAge(User u) =>
Either.cond(() => u.age > 35, () => u, () => 'User is too young!');
const candidate = User('Harrison', 'Harry', 30);
final validatedCandidate = validateName(candidate)
.flatMap(validateAlias)
.flatMap(validateAge); // Left(User is too young!)
fold
Lastly, when you want to create a summary value from the Either
depending
on whether it's a Left
or Right
, the fold
method makes it easy:
final foldLeft = const Left<bool, int>(false).fold(
(boolean) => 'bool value is: $boolean',
(integer) => 'int value is: $integer',
);
As specified in the fold
function signature, each function provided
must return a value of the same type.
Either has a lot of other useful combinators to make using them easy and expressive! Check out the API to explore them.