• No results found

The Option/Maybe and Either types

In document FunctionalPHP_2 (Page 94-102)

As hinted before, our solution is to use a return type that contains the wanted value or something else in case of error. Those kinds of data structures are called union types. A union can contain values of different types, but only one at a time.

Let's start with the easiest of both union types we will see in this chapter. As always, naming is a difficult thing in computer science and people came up with different names to designate mostly the same structure:

Haskell calls it the Maybe type, as does Idris

Scala calls it the Option type, as does OCaml, Rust, and ML

Since version 8, Java has an Optional type, as does Swift and the next specification of C++

Personally, I prefer the denomination Maybe as I consider an option to be something else.

The remainder of the book will thus use this, except when a specific library has a type called Option.

The Maybe type is special in the sense that it can either hold a value of a particular type or the equivalent of nothing, or if you prefer, the null value. In Haskell, those two possible values are called Just and Nothing. In Scala, it is Some and None because Nothing is already used to designate the type equivalent of the value null.

Libraries implementing only a Maybe or Option type exist for PHP, and some of the libraries presented later in this chapter also ship with such types. But for the sake of correctly understanding how they work and their power, we will implement our own.

Let us reiterate our goals first:

Enforce error management so that no errors can bubble up to the end user Avoid boilerplate or complex code structure

Advertised in the signature of our function

Avoid any risk of mistaking the error for a correct result

If you type hint your function return value using the type that we will create in a few moments, you are taking care of our third goal. The presence of two distinct possibilities, the Just and Nothing values, ensure that you cannot mistake a valid result for an error. To make sure we don't end up with an erroneous value somewhere along the line, we must ensure that we cannot get a value from our new type without specifying a default if it is the Nothing value. And, concerning our second goal, we will see if we can write something nice:

<?php

abstract class Maybe {

public static function just($value): Just {

return new Just($value);

}

public static function nothing(): Nothing {

return Nothing::get();

}

abstract public function isJust(): bool;

abstract public function isNothing(): bool;

abstract public function getOrElse($default);

}

Our class has two static helper methods to create two instances of our soon-to-come child classes representing our two possible states. The Nothing value will be implemented as a singleton for performance reasons; since it will never hold any values, it is safe to do it this way.

The most important part of our class is an abstract getOrElse function, which will force anyone wanting to get a value to also pass a default that will get returned if we have none.

This way, we can enforce that a valid value will be returned even in the case of error.

Obviously, you could pass the value null as the default, since PHP has no mechanism to enforce something else, but this would be akin to shooting yourself in the foot:

<?php

final class Just extends Maybe {

private $value;

public function __construct($value) {

$this->value = $value;

}

public function isJust(): bool {

return true;

}

public function isNothing(): bool {

return false;

}

public function getOrElse($default) {

return $this->value;

} }

Our Just class is pretty simple; a constructor and a getter:

<?php

final class Nothing extends Maybe {

public function isJust(): bool {

return false;

}

public function isNothing(): bool {

return true;

}

public function getOrElse($default) {

return $default;

} }

If you don't take the part about being a singleton into account, the Nothing class is even simpler because the getOrElse function will always return the default value no matter what. For those wondering, it is a deliberate choice to keep the constructor public. It has absolutely no consequences if someone wants to create a Nothing instance directly, so why bother?

Let's test our new Maybe type:

<?php

$hello = Maybe::just("Hello World !");

$nothing = Maybe::nothing();

Everything seems to be working great. The need for boilerplate can be improved though. At this point, every time you want to instantiate a new Maybe type, you need to check the value you have and choose between the Some and Nothing values.

Also, it might happen that you need to apply some functions to the value before passing it further without knowing at this point what default value is best. As it would be

cumbersome to get the value with some temporary default before creating a new Maybe type right behind, let's try to fix this aspect as well:

<?php

abstract class Maybe {

// [...]

public static function fromValue($value, $nullValue = null) {

return $value === $nullValue ? self::nothing() :

self::just($value);

}

abstract public function map(callable $f): Maybe;

}

final class Just extends Maybe {

// [...]

public function map(callable $f): Maybe {

return new self($f($this->value));

} }

final class Nothing extends Maybe {

// [...]

public function map(callable $f): Maybe {

return $this;

} }

In order to have a somewhat coherent naming for utility methods, we use the same name as for functions working with collections. In a way, you can consider a Maybe type like a list with either one or no value. Let's add some other utility methods based on the same assumption:

{

// [...]

abstract public function orElse(Maybe $m): Maybe;

abstract public function flatMap(callable $f): Maybe;

abstract public function filter(callable $f): Maybe;

}

final class Just extends Maybe {

// [...]

public function orElse(Maybe $m): Maybe {

return $this;

}

public function flatMap(callable $f): Maybe {

return $f($this->value);

}

public function filter(callable $f): Maybe {

return $f($this->value) ? $this : Maybe::nothing();

} }

final class Nothing extends Maybe {

// [...]

public function orElse(Maybe $m): Maybe {

return $m;

}

public function flatMap(callable $f): Maybe {

return $this;

}

public function filter(callable $f): Maybe {

return $this;

} }

We have added three new methods to our implementation:

The orElse method returns the current value if there is one, or the given value if it was Nothing. This allows us to easily get data from multiple possible sources.

The flatMap method applies a callable to our value but does not wrap it inside a Maybe class. It is the responsibility of the callable to return a Maybe class itself.

The filter method applies the given predicate to the value. If the predicate returns true value, we keep the value; otherwise, we return the value Nothing.

Now that we have implemented a working Maybe type, let's see how we can use it to get rid of error and null management easily. Imagine we want to display information about the connected user in the upper-right corner of our application. Without a Maybe type, you do something like the following:

<?php

$user = getCurrentUser();

$name = $user == null ? 'Guest' : $user->name;

echo sprintf("Welcome %s", $name);

// Welcome John

Here, we only use the name, so we can limit ourselves to one null check. If we need more information from the user, the usual method is to use a pattern that is sometimes called the Null object pattern. In our case, our Null object will be an instance of AnonymousUser method:

<?php

$user = getCurrentUser();

if($user == null) {

$user = new AnonymousUser();

}

echo sprintf("Welcome %s", $user->name);

// Welcome John

Now let's try to do the same with our Maybe type:

<?php

$user = Maybe::fromValue(getCurrentUser());

$name = $user->map(function(User $u) { return $u->name;

})->getOrElse('Guest');

echo sprintf("Welcome %s", $name);

// Welcome John

echo sprintf("Welcome %s", $user->getOrElse(new AnonymousUser())->name);

// Welcome John

The first version might not be much better, as we have had to create a new function to extract the name. But let's keep in mind that you could do any number of treatments on your object before needing to extract a final value. Also, most functional libraries we present later provide helper methods to get value from objects in a simpler way.

You can also easily call a chain of methods until one of them returns a value. Say you want to display a dashboard, but those can be redefined on a per-group and per-level basis. Let's compare how our two methods fare.

First, the null value check approach:

<?php

$dashboard = getUserDashboard();

if($dashboard == null) {

$dashboard = getGroupDashboard();

}

if($dashboard == null) {

$dashboard = getDashboard();

}

And now, using Maybe type:

<?php

/* We assume the dashboards method now return Maybe instances */

$dashboard = getUserDashboard()

->orElse(getGroupDashboard()) ->orElse(getDashboard());

I think the more readable one is easier to determine!

Finally, let us demonstrate a little example on how we could chain multiple calls on a Maybe instance without having to check whether we currently have a value or not. The chosen example is probably a bit silly, but it shows what is possible:

<?php

$num = Maybe::fromValue(42);

$val = $num->map(function($n) { return $n * 2; }) ->filter(function($n) { return $n < 80; }) ->map(function($n) { return $n + 10; }) ->orElse(Maybe::fromValue(99))

->map(function($n) { return $n / 3; }) ->getOrElse(0);

echo $val;

// 33

The power of our Maybe type is that we have never had to consider whether the instance contained a value. We were just able to apply functions to it until finally, extracting the final value with the getOrElse method.

In document FunctionalPHP_2 (Page 94-102)

Related documents