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.
Copy Option :: some ( 2 ) ;
some ( 2 ) ; // alias
static function none(): Option<T>
Returns an option with no value.
Copy Option :: none () ;
none () ; // alias
function isSome(): bool
Returns true if the option is with some value.
Copy assert ( true === some ( 2 ) -> isSome ()) ;
assert ( false === none () -> isSome ()) ;
function isNone(): bool
Returns true if the option is with no value.
Copy assert ( true === none () -> isNone ()) ;
assert ( false === some ( 2 ) -> isNone ()) ;
function equals(Option<T> $other): bool
Returns true if this option equals other option.
Copy 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.
Copy 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.
Copy 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.
Copy 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.
Copy 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.
Copy 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.
Copy assert ( 2 === some ( 2 ) -> get ()) ;
none () -> get () ; // throws an exception
function getOr(T $value): T
Returns some value or the provided value on none.
Copy assert ( 2 === some ( 2 ) -> getOr ( 3 )) ;
assert ( 3 === none () -> getOr ( 3 )) ;
function getOrNull(): ?T
Returns some value or null
on none.
Copy 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.
Copy 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.
Copy 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.
Copy 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:
Copy $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>
.
Copy 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.
Structural Functional
Copy 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;
} ) ;
}
}
Copy class UserService
{
public const USERNAME_ALREADY_TAKEN = 'username_already_taken' ;
/** @return Result < User , Error[]> */
public function registerUser ( UserRegistrationData $data) : Result
{
return $this -> userRepository -> find ( $data -> username ) -> mapOrElse (
fn () => User :: register ( $data -> username , $this -> passwordEncoder -> encode ( $data -> password ))
-> andThen ( function ( User $user) {
$this -> userRepository -> save ( $user ) ;
return $user;
} ),
fn () => err ( [ new Error (
sprintf ( 'Username "%s" is already taken.' , $data -> username ),
self:: USERNAME_ALREADY_TAKEN
)] )
) ;
}
}
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.