Either¶
The Either
monad always represents a value but it's either on a right side or left side.
If you've understood Maybe
, it's an Either
with the Maybe
value on the right side or null
as the value on the left side.
In essence:
use Innmind\Immutable\Either;
$right = Maybe::just(42);
$left = Maybe::nothing();
// becomes
$right = Either::right(42);
$left = Either::left(null);
It is usually used to express a value for a nominal case on the right side and the errors that may occur on the left side. This means that it replaces the use of Exception
s.
Each example will show how to use Either
and the imperative equivalent in plain old PHP via Exception
s.
Tip
This is because Maybe
and Either
are very similar that you can switch for one type to another via $maybe->either()
(1) or $either->maybe()
(2).
- then the left side is
null
- the left value is thrown away
Usage¶
Let's say you want to create a user from an email and the function must fail in case the email already exist. You could do:
/**
* @return Either<EmailAlreadyUsed, User>
*/
function createUser(string $email, string $name): Either {
if (/* some condition to check if email is already known*/) {
return Either::left(new EmailAlreadyUsed);
}
/* code to insert the user in a db */
$user = new User($email, $name);
return Either::right($user);
}
createUser('foo@example.com', 'John Doe')->match(
static fn(User $user) => doStuff($user),
static fn(EmailAlreadyUsed $error) => doOtherStuff(),
);
/**
* @throws EmailAlreadyUsed
* @return User
*/
function createUser(string $email, string $name): User {
if (/* some condition to check if email is already known*/) {
throw new EmailAlreadyUsed;
}
/* code to insert the user in a db */
$user = new User($email, $name);
return $user;
}
try {
$user = createUser('foo@example.com', 'John Doe');
doStuff($user);
} catch (EmailAlreadyUsed $e) {
doOtherStuff();
}
Here we use a string
to represent an email, instead we should use an object to abstract it to make sure the value is indeed an email.
final class Email
{
/**
* @return Either<InvalidEmail, self>
*/
public static function of(string $value): Either
{
if (/* check value */) {
return Either::right(new self($value));
}
return Either::left(new InvalidEmail);
}
}
Email::of('foo@example.com')
->flatMap(static fn(Email $email) => createUser($email, 'John Doe'))
->match(
static fn(User $user) => doStuff($user),
static fn(InvalidEmail | EmailAlreadyUsed $error) => doOtherStuff(),
);
final class Email
{
/**
* @throws InvalidEmail
* @return self
*/
public static function of(string $value): self
{
if (/* check value */) {
return new self($value);
}
throw new InvalidEmail;
}
}
try {
$email = Email::of('foo@example.com');
$user = createUser($email, 'John Doe');
doStuff($user);
} catch (InvalidEmail | EmailAlreadyUsed $e) {
doOtherStuff();
}
Both approaches seem very similar but there's a big advantage to Either
: a static analysis tool understands the flow of errors and can tell you if when calling match
you don't handle all possible error values. No tool can help you do the same with exceptions.
Just like Maybe
you can recover in case of an error via the otherwise
method. For example in the case the email is already used, instead of failing we can decide to update the stored user.
In all examples you've seen the use of flatMap
but you can also use the map
to modify the value on the right side. And there's a leftMap
to modify the value on the left side.
In the ecosystem¶
Either
is used when an action may have multiple cases of errors that you should handle, such as HTTP calls or when working with queues.
But the beauty is that if you don't want to deal with the different errors you can throw them away by converting the Either
to a Maybe
via $either->maybe()
.
Like Maybe
and Sequence
is has a deferred mode that allows to postpone some actions as you'll see in the concurrent HTTP calls chapter.