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