Perhaps — Functional Programming in PHP (Part 5)

Viktor Daróczi
10 min readApr 10, 2021

This is the fifth part of a series of articles about Functional Programming in PHP. Previous parts:

Introduction
Currying
Function Composition
More Functions

The Reluctant Code

You won’t admit you love me.
And so how am I ever to know?
You only tell me
Perhaps, perhaps, perhaps.

Photo by Matt Seymour on Unsplash

So far we only saw strong-minded functions that are explicit and resolute. It turns out that our code also can be reluctant. And by that I don’t mean reluctant to reveal the exact location and source of bugs in our program. We know that way too well. In fact, one of PHP8’s cool new features is the nullsafe operator, which doesn’t hesitate to express how puzzled it is about the outcome of our code:

$country = $session?->user?->getAddress()?->country;

Three question marks? Come on! It’s the programming language’s way to tell us it had no idea what that expression might end up with.

In fact, it’s not about nondeterminism, but — as the name suggests — safety. And also convenience. It spares us from writing several embedded ifs (which we all hate, do we?), while preserving the same effect:

$country =  null;

if ($session !== null) {
$user = $session->user;

if ($user !== null) {
$address = $user->getAddress();

if ($address !== null) {
$country = $address->country;
}
}
}

From this expanded code, we can tell, that $country can end up containing only a null value, if any of these conditions fail. And also that it won’t bother resolving anything after the first failed condition.

We’re getting closer. It’s not half bad, but we can do it even better.

Putting it on the map

One big shortcoming of the above example that our variable still can contain a null value, and we’re forced to check that from there on, regardless of how easy that check might be. Another big shortcoming is that we can’t just do any operations on the variable. This will not work: doSomething($var?).

A more sophisticated solution would be to put that nullable value into a container on which we can make operations in a functional way regardless of the actual value. We already know an example of this in array_map which takes a callable and an array, and returns a new array with elements generated by calling the callable on each of the elements of the original array. The interesting feature of this function is that we can call it with an empty array as well, and it won’t panic, just returns an empty array as if nothing happened. No exceptions, errors, warnings thrown. No checks forced to perform. That’s nice.

However, when we feed array_map with null value, we’re rewarded with a warning:

PHP Warning:  Uncaught TypeError: array_map(): Argument #2 ($array) must be of type array, null given

What we’d need here is a similar function which performs any operations on null value without batting an eye, while actually doing nothing.

Enter Maybe

PHP hasn’t done us this favor yet, but we can write such a container easily ourselves. Writing it is not the big deal, using it is a game changer. Let’s call it Perhaps. Just kidding. It’s actually called Maybe (Haskell) or Option(al) (Scala, Java).

Think about Maybe as a container for nullable values, which lets us apply functions on the stored value, if it’s present.

Notice the map method. It’s like array_map for arrays. A ‘maybe_map’ for nullable values. The important thing here, that similarly to array_map which returns a new array, even if the original array was empty, Maybe will also return Maybe, even if the value is not present.

Maybe::of(null)->map(fn($x) => $x . $x) // Nothing

Where ‘Nothing’ is an empty Maybe, and therefore still capable of mapping functions, with map. So this is totally valid:

Maybe::of($session)
->map(fn($session) => $session->user)
->map(fn($user) => $user->getAddress())
->map(fn($address) => $address->country)
// Maybe(country) or Nothing

Congratulations, you’ve just achieved in 3 lines, what PHP managed in 1! But at least you probably can see the pattern here. The map function always returns a Maybe, no matter what, so we can map functions further. We could even use our prop function from part 4 here:

Maybe::of($session)
->map(prop('user'))
->map(prop('address'))
->map(prop('country'))

Looks better, but still not as expressive as using nullsafe operators. So what’s the point of using Maybe at all?

Well, first of all, we’re not restricted to use properties and methods that are supported by the variable. Even prop here is just a generic function, it could be any other function that expects that value, and only that value. We don’t need to prepare them for null value, because they will never be called with a null value.

Furthermore, the map method makes Maybe a functor. Functor come from category theory, and it obeys some laws, that we only care as much as they make our code predictable and easier to reason about. But not only that! Using those obscure principles from Math we didn’t bother mastering, we can equip our class with superpowers. These superpowers let us control side effects the way map lets us apply functions on null value without breaking our application. Wait, there’s more! Maybe is not the only container we can upgrade with such methods, and they all behave predictably thanks to those laws regardless of what kind of data or effect they are storing.

Yes, you’ve read that right. We can also store effects in containers, and manipulate those effects without releasing them, if we don’t want to.

Let’s see that in action!

Embrace Failure

Imagine you have an old API, developed by some programmers, who abandoned the industry long time ago, because they fed up maintaining the legacy code they were responsible for. The company tried to bribe some young titans to fix the everlasting pesky bugs and crashes this API tend to push out of itself, but they weren’t even willing to poke it with a stick, because it smelled, and it didn’t resemble to anything from the latest trends. The company then realized they have to embrace that the API fails randomly, and nobody can fix it anymore. They put the server running the API in a distant room in the basement behind a red door, and hired some developers to write a modern shiny interface to the old API, that wouldn’t fail. This new interface, instead of crashing, would just release a company color themed toast message to the users, that despite its best effort, the old API failed again, and gently recommends to try the operation again, as soon as they finished steering their latte macchiato.

Our old API:

random_int(0, 1) ? 'Value' : null

From this value, we should generate our new end-user license agreement version 1, so the skilled developers wrote the following brilliant function to do just that:

function toEula() {
return pipe(
fn($s) => [ head($s), tail($s) ],
fn($a) => strtolower($a[0]) . strtoupper($a[1]),
fn($s) => strrev($s),
fn($s) => $s . '1'
);
}

The problem is, that when this function is called with null value like this:

toEula()($nullValue)

Then even the new application crashes with the following error:

PHP Fatal error:  Uncaught TypeError: FunctionalAPI::head(): Argument #1 ($xs) must be of type object|array|string, null given

Ouch! Head, have you just betrayed us?! Nevermind, because our talented developers read my article, and after discussing the issue in long meetings they decided to use Maybe for any results provided by the old API, and they cut the Gordian knot:

Maybe::of(random_int(0, 1) ? 'Value' : null)

This smart wrapper technique provided just the required resiliency. If we check the commit history of the new repository, we will see that they used this wrapper two different ways:

$valueFromOldAPI
->map(fn($s) => [ f::head($s), f::tail($s) ])
->map(fn($a) => strtolower($a[0]) . strtoupper($a[1]))
->map(fn($s) => strrev($s))
->map(fn($s) => $s . '1')

and later:

$valueFromOldAPI->map(toEula())

But no matter which solution they took, the application remained resilient, and never crashed again.

The only thing left was the nice, shiny, company color themed toast message about the success or failure of the operation. That was easy:

maybe(
'Old API failed again, please try again after you finished steering your latte macchiato',
fn($s) => toEula()($s) . ' is ready',
$valueFromOldAPI
)

Everybody was super happy with the new modern interface. The maybe function was printed and framed, and hanged on the company’s entrance corridor wall:

Of course the developers received a huge bonus, and everybody lived happily ever after.

So much for programmer tales.

Maybe Maybe it’s not that big deal after all, and we should rather call it Perhaps instead. NB asking your marketing department about that. But as promised, the uncertainty controlling technique illustrated above is not restricted to use cases that can be easily solved with if-else blocks.

There’s a reason why programming languages keep on adding tools to simplify or replace if-else blocks, for example switch statements, match statements, ternary operator, null coalescing operator, just to name a few. Using these constructs gets more and more complicated with adding more uncertain values to the equation. Containers, mapping and function application provide a seamless method to deal with values that might depend on each other without spraying ifs all over our code. Furthermore, this behavior will remain consistent even with containers designed to store slightly different values like either-or choices or effects. They act like a contract, and guarantee that you can use them the same way you’re already familiar with, while they provide you full control over the contained values. Let’s see some examples.

Functional design patterns

You might have a factory that may return a function that operates on some value. We wrap this into Maybe, so that we can handle errors the familiar way demonstrated above. Then we realize that even the value is uncertain, and we wrap that also in Maybe. It would be great if we could just combine the two uncertain values in a way that the contained function operates on the wrapped value, while it also wraps the result in Maybe.

Your wish is my command. Here’s how you can do that with the support of applicatives:

Maybe::of(fn($s) => strtoupper($s))->apply($valueFromOldAPI)
// Maybe(VALUE)

You may think that ‘it’s fair enough, but I bet it takes huge dependent classes to make it work’. If you do so, you’re wrong. It’s just one line:

public function apply($f) {
return $this->isNothing() ? $this : $f->map($this->value);
}

This is one of the methods that I meant to be implemented for Maybe later. Here it is. And thanks to map, it never fails. It takes a functor, and then maps the value stored in its instance to that. Do you care what a functor is? It’s just our other container which happens to have a map method. Let’s say it’s the Mappable interface. Whatever.

We could even define those interfaces, and force our parameters to implement them:

interface Functor {
public function map(callable $fn): Functor;
}
class Maybe implements Functor {
...
public function apply(Functor $f) {

And because Functor requires the map method to be implemented, it will work on any container that implements Functor, even if it’s not Maybe!

OK, but what if you have even more unstable values, and want to combine them in the same way, so that they all depend on each other, while wrapping the result in Maybe. Help yourself:

liftA2(gimmeTwoValuesMaybe(), $valueFromOldAPI, $unstableValue)

where gimmeTwoValuesMaybe is a curried function that expects two random values and does something with them, but maybe it doesn’t do anything if one of the values is missing. Again, liftA2 — despite having such a cryptic name — is just one line to implement. What it does is taking a function which expects two values from containers, and creates a new container for the result of the applied function. And it plays well with Maybe, but would do the same with IO, and Either as well. Although you may not know what these containers are just yet, you already can count on, that they will behave the same way, because they implement the same interfaces:

curry(fn(callable $fn, Applicative $a1, Applicative $a2) => $a1->map($fn)->apply($a2))

where Applicative is just an interface that extends Functor, and requires both map and apply methods to be implemented.

Now what if you would create new functions by piping or composing other existing functions, but some of those functions return a value in a container, which in turn will be put into a container, like this:

changeValueMaybe(getValueMaybe()) // Maybe(Maybe(value))

and you want just one of them, but don’t mind if this all can be done with just one function call. Here you are:

pipe(
safeProp('user'),
chain(safeProp('address')),
chain(safeProp('country'))
)

This is the same logic from the introduction. By the way, safeProp is just prop that returns the value wrapped into Maybe. Instead of returning Maybe wrapped in Maybe, chain (aka flatMap) makes sure that we end up with only one layer of wrapping.

chain(Maybe::of(Maybe::of($x))) // Maybe($x)

Implementing chain for Maybe is as easy as it gets. We apply the passed function on the value with map, and if the result is meaningful, we return its value, otherwise we return the empty container. In either cases, the result is one single Maybe.

public function chain(callable $fn) {
return $this->map($fn)->join();
}
public function join() {
return $this->isNothing() ? $this : $this->value;
}

It turns out that implementing chain (in addition to what we already have) also makes our container a Monad, but you shouldn’t be scared of such names by now, because you already know it’s just like implementing an interface. We could just as well call it FlatMappable or Chainable.

This way we can keep on combining, composing and organizing functions and along with it our business logic, without the risk of having to apply some side effects in the meantime, because they will always give us the final word.

Hence, if a function returns a container, it’s like it was saying to us that, this can be tricky, but it’s up to you when you want to bear the consequences. Maybe tells us that a value may not be there, but it’s up to us when we want to deal with that, and we’re fine to postpone it to when we finally would need it. IO tells us, that there are some side effects in this container, but none of it was launched just yet, it’s up to us to decide when to release it. Either tells us, that an error may have occurred during some operation, so the container either contains the result, or the reason why it failed, but it’s entirely up to us when we want to process it. Task tells us… well, you already know Task, it’s called Promise in JavaScript and Future in Java, they just forgot to implement the methods that would make them act like a decent FP container. Meh.

The point is, even if you don’t know what kind of containers are there, if they comfort these contracts, then you can rely on them, and you can use them the same way you’re familiar with from other containers.

In the next part we will check some of these containers, what are they for, and how to use them, but be prepared they will look familiar, so stay tuned!

--

--

Viktor Daróczi

A software engineer focusing on the fun part of computing.