Option
Represents either some value or none.
Overview
Typically, to mark that a function return value, an argument, or a property has some value or not, you'd use the nullable type declaration. Most of the time it works just fine, but can sometimes be unpleasant when dealing directly with a null
value that we weren't expecting.
Instead of playing with null
values directly you can use the Option::none()
and to represent some value just call Option::some($value)
. Later, in the code example, I'll show how we can use the Option
to refactor a small fragment of the Result
code example.
API
static function some(T $value): Option<T>
Returns an option with some value.
Option::some(2);
some(2); // alias
static function none(): Option<T>
Returns an option with no value.
Option::none();
none(); // alias
function isSome(): bool
Returns true if the option is with some value.
assert(true === some(2)->isSome());
assert(false === none()->isSome());
function isNone(): bool
Returns true if the option is with no value.
assert(true === none()->isNone());
assert(false === some(2)->isNone());
function equals(Option<T> $other): bool
Returns true if this option equals other option.
assert(true === some(2)->equals(some(2)));
assert(true === none()->equals(none()));
assert(false === some(2)->equals(some('2')));
assert(false === none()->equals(some(2)));
function andThen<U>(callable(T): U $fn): Option<U>
Maps some value by using the provided callback and returns a new option. Returns none if the option is none.
function mul(int $value): int { return 2 * $value; }
assert(4 === some(2)->andThen('mul')->get());
assert(true === none()->andThen('mul')->isNone());
function andThenTo<U>(callable(T): Option<U> $fn): Option<U>
Maps some value by using the provided callback which returns a new option that can be with some or no value. Returns none if the option is none.
function mul(int $value): Option { return some(2 * $value); }
assert(8 === some(2)->andThenTo('mul')->andThenTo('mul')->get());
assert(true === some(2)->andThenTo('mul')->andThenTo('none')->isNone());
assert(true === some(2)->andThenTo('none')->andThenTo('mul')->isNone());
assert(true === none()->andThenTo('mul')->andThenTo('mul')->isNone());
function orElseTo<U>(callable(): Option<T> $fn): Option<T>
If the option is with no value, it calls the provided callback to return a new option. Otherwise, it returns the untouched option.
function def(): Option { return some(3); }
assert(3 === none()->orElseTo('none')->orElseTo('def')->get());
assert(3 === none()->orElseTo('def')->orElseTo('none')->get());
assert(5 === some(5)->orElseTo('def')->orElseTo('none')->get());
assert(true === none()->orElseTo('none')->orElseTo('none')->isNone());
function mapOr<U>(U $value, callable(T): U $someFn): U
If the option is with some value, it maps its value using the provided $someFn
callback, otherwise it returns the provided value.
function mul(int $value): int { return 2 * $value; }
assert(4 === some(2)->mapOrElse(3, 'mul'));
assert(3 === none()->mapOrElse(3, 'mul'));
function mapOrElse<U>(callable(): U $noneFn, callable(T): U $someFn): U
If the option is with some value, it maps its value using the provided $someFn
callback, otherwise it calls $noneFn
callback.
function mul(int $value): int { return 2 * $value; }
assert(4 === some(2)->mapOrElse(3, 'mul'));
assert(3 === none()->mapOrElse(3, 'mul'));
function get(): T
Returns some value.
assert(2 === some(2)->get());
none()->get(); // throws an exception
function getOr(T $value): T
Returns some value or the provided value on none.
assert(2 === some(2)->getOr(3));
assert(3 === none()->getOr(3));
function getOrNull(): ?T
Returns some value or null
on none.
assert(2 === some(2)->getOrNull());
assert(null === none()->getOrNull());
function getOrElse(callable(): T $fn): T
Returns some value or a value returned by the provided callback on none.
assert(2 === some(2)->getOrNull());
assert(null === none()->getOrNull());
function asOkOr<E>(E $err): Result<T, E>
Returns Result::Ok(T)
where T
is some value or Result::Err(E)
where E
is an error value.
assert(2 === some(2)->asOkOr(3)->get());
assert(3 === none()->asOkOr(3)->getErr());
Code example
Let's recall the code example from the Result
type.
class UserService
{
public const USERNAME_ALREADY_TAKEN = 'username_already_taken';
/** @return Result<User, Error[]> */
public function registerUser(UserRegistrationData $data): Result
{
$user = $this->userRepository->find($data->username);
if ($user) {
return err([new Error(
sprintf('Username "%s" is already taken.', $data->username),
self::USERNAME_ALREADY_TAKEN
)]);
}
return User::register($data->username, $this->passwordEncoder->encode($data->password))
->andThen(function (User $user) {
$this->userRepository->save($user);
return $user;
});
}
}
It looks nice, but we can still do more here. As you can guess, it's time for the Option
type to shine!
We're going to refactor the following part of the code:
$user = $this->userRepository->find($data->username);
if ($user) {
return err([new Error(
sprintf('Username "%s" is already taken.', $data->username),
self::USERNAME_ALREADY_TAKEN
)]);
}
Let's make the UserRepositoryInterface::find()
method to return the Option<User>
.
interface UserRepositoryInterface
{
/** @return Option<User> */
public function find(string $username): Option;
public function save(User $user): void;
}
Now, to use the new refactored version of the user repository, we can do it in two ways.
class UserService
{
public const USERNAME_ALREADY_TAKEN = 'username_already_taken';
/** @return Result<User, Error[]> */
public function registerUser(UserRegistrationData $data): Result
{
$op = $this->userRepository->find($data->username);
if ($op->isSome()) {
return err([new Error(
sprintf('Username "%s" is already taken.', $data->username),
self::USERNAME_ALREADY_TAKEN
)]);
}
return User::register($data->username, $this->passwordEncoder->encode($data->password))
->andThen(function (User $user) {
$this->userRepository->save($user);
return $user;
});
}
}
There has been a lot of discussion about the functional version as some have argued it's less readable because it has a more compact form. I think everyone can decide for themselves whether they prefer the functional version or the structural version. The library doesn't impose anything.
I hope that at least this example showed you where you can use the Option
type and how easily you can switch between the Result
and the Option
types.
Last updated