The Irresistible Container — Functional Programming in PHP (Part 6)

Viktor Daróczi
8 min readApr 24, 2022

--

Photo by Timelab Pro on Unsplash

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

Introduction
Currying
Function Composition
More Functions
Perhaps

The concept of containing stuff is so powerful we can’t resist putting every possible things into containers. Once something is contained it’s contained. Then we can manage it like a boss. In the previous article we examined one of the problematic matters that we were still able to contain with a clever container called Maybe. This container did a great job managing dead-or-alive values, but we have other flaky pieces of code to worry about. The good thing in containers is that they are uniform and obey some rules. Knowing those rules, we can rely on the containers and that they will always hold our materials well, letting us do all kinds of manipulations with them and their contents.

Biohazard

One of the essential features of the containers is that they can contain your stuff even if it’s rotten. The smell and biohazard won’t threaten you as long as you keep the jar’s lid closed. This beautiful concept applies to the risky world of programming as well.

So what if you try to divide two numbers, and the divisor happens to be zero? Nobody wants to play with it anymore just because it picked the wrong side of an operation?

¯\_(ツ)_/¯

But worry not, dear coding enthusiast, because containers are to the rescue. We will [pretend to] invent a container to hold possibly spoiled operations and — even better — the reason why/how things went wrong.

You can yawn at this point saying that we already have exceptions to throw around like there’s no tomorrow, but let me remind you that they tend to be quite impolite and impatient when it comes to handling them.

Our container on the other hand can wait until the end of times, without urging us the slightest to face the inconvenient truth of something being gone wrong. Furthermore, it let’s us do our business as usual (provided we use its interface) as if nothing had happened. It offers full control over when we want to deal with the problem. It does not come to replace try-catch blocks, but to help us cope with exceptions wherever we feel like and not where we’re forced to.

For this to work, we need to split the container into two personalities. One of them is containing the correct result, a tasty compote. The other one contains the hazardous spoiled version of it, but we don’t care which one is there as long as it’s in the container. We still can move and operate on it as long as we don’t open it.

Let’s see that in action:

Either::of("this")
->map(fn($str) => "Use $str!")

We have a result like a string, and then we want to embed it into another string, so we simply call map on it as usual. This returns Right('Use this!'), where Right is the child class of our error tracking container, Either, that contains proper results. But what if an error happened, and we get a bold error message instead? We don’t want to embed that, do we?

left("ignore this")
->map(fn($str) => "Don't $str!")

Here left is a helper function to create an instance of the child class of Either that is designed to hold the spoiled value or error message. As you can see, we can still call map on it, but the disastrous function call that would mix the rotten ingredient into our ambitious masterpiece of a meal will never be called. Our container politely refuses to do anything, without getting hysterical, and saves the day. The result is Left('ignore this'), which is still a subclass of Either therefore supporting any operations like its parent.

We can parse JSON like this, even piping many consecutive operations on the value that may not be there at all, but at least granted to carry a message in case of failure:

parseJson(valid_json)->map(getFirstStuff())                         // =>  Right('first')
parseJson(invalid_json)->map(getFirstStuff()) // => Left('Syntax error')
pipe( parseJson, either( id, getFirstStuff ), print )(valid_json) // => first
pipe( parseJson, either( id, getFirstStuff ), print )(invalid_json) // => Syntax error

(In case some parts of this code looks obscure, I can offer a hint that pipe just calls the given functions one after another feeding the result of the first one into the next and so on, while id just returns the same value it received as an input, because it fits well in pipes.)

When we reach the point where we really need to distinguish good from bad, we can conveniently probe Either whether it’s Right or Left, and present the result accordingly. Here we just printed whatever we got, but imagine putting the result into a frame, or showing a red error message when there’s nothing else left to do. And we don’t need to break the flow with any checks before. Both children of Either plays nice always.

By the way, either is just a helper function that accepts two function parameters and runs either one of those depending on whether the result is Left or Right:

I’m afraid I talk too much about the details here. The point is, that with some lightweight classes and even thinner helper functions, we can contain and manage errors to the infinity, and we have full control over when we want to face the consequences.

Collateral Damage

Side effects are said to be bad for any piece of code. At the same time side effects are unavoidable if we’re planning to perform anything whatsoever. To resolve this shameless conflict — as you would expect — we can introduce some containers.

Imagine you want to perform some tasks, but also don’t want to perform them at just any time, but instead you want to operate on them without actually breaking the logic of your application, and with a clear separation of parts of the code that actually can have a side effect from parts that don’t and therefore easily testable.

We can do all kinds of collateral damage by carelessly running code that has hidden side effects. To contain the risks, we use containers. These containers will allow us to do our functional mumbo jumbo without actually interfering with the external world. How is that possible? This is just the thin foil that you can pull out from a battery included device, that prevents the battery from being drained before it’s shipped. Once you pull it out, the device gets fully functional. But before that, it’s granted to preserve your resources.

This is exactly that Task and IO does.

Credit: Jens Segers

Let’s say I want to download the internet as I always do, and want to do some operations on the content, like counting all the Nyan cat occurrences. This would take some time, and I want to make sure that everything is prepared correctly before I even start. I also want my code to indicate clearly where the actual network request is initiated, and that all the code that has no side effects are separated from that in an obvious manner.

IO lets us contain such operations and provides means to work on the result without actually performing the sensitive operations. As always, we can manipulate the result safely in the code — nothing will actually happen until we explicitly say so. It’s our way of pulling the plastic battery insulation out, and let the device work.

Working with containers is often like passing a to-do list around. First I write it down, that I want to download the internet. Then I give the list to the person who’s responsible for planning what will happen with the internet once I’ve downloaded it, and adds an item to it:

IO::of(fn() => downloadInternet())
->map(findAllNyanCatOccurrences())
// a curried function expecting the internet

Then passes the list to the next stakeholder who adds another item:

IO::of(fn() => downloadInternet())
->map(findAllNyanCatOccurrences())
->map(curriedCount()) // returns IO

Maybe we can also add showing the result on a chalkboard or whatever. But note that nothing on the list has happened yet. We just know what will happen, and can even test the interaction between the parties, whether the finder added the task to the list correctly, and whether counting was added too. All that gets exchanged is the to-do list, or recipe. Everything is pure, everything is safe and testable. But when we pull the trigger, it all gets executed: IO::of($heavyLiftingFn)->runIO() — this will call the function stored in $heavyLiftingFn.

IO will not pay attention on whether the operation carried out was successful or not. It focuses only on that it will have side effects, and provides a trigger to execute it safely. This negligence made it unfavorable among programming languages that are slowly adopting functional features, but as always, the most powerful features derive from the simplest concepts.

Task on the other hand is typically used for things that can fail, and its trigger is fork that being loyal to its name will split the flow of our program depending on the result of the task. So we provide two functions, one for each case, success and failure. While runIO() accepted no parameters, fork expects two functions, $reject and $resolve. It’s up to the task to call either one of those, because only the task knows whether it could carry out the operation successfully or not (call_user_func omitted for clarity):

// PURE PART
$myTask = new Task(
fn($reject, $resolve) => random_int(0, 1) < 1
? $reject('insufficient funds')
: $resolve('riches')
);
// nothing happened yet
// IMPURE PART
$myTask->fork(
fn($reason) => print($reason),
// would print 'insufficient funds'
fn($result) => print($result) // would print 'riches'
);

Code written with these containers are typically divided into pure and impure parts, where impure means the trigger is pulled there, and interaction with the external world will occur between those lines. Whereas the pure part is fully transparent and easily testable with simple unit tests that don’t require mocks or other Frankensteinian products of software engineering.

Photo by Sandie Clarke on Unsplash

At this point I don’t recommend to worry about the exact implementation of these containers. Enough to know that they all will obey to the same rules, i.e. implement the same interfaces, and therefore are able to be passed around in the code and getting applied functions on them.
While the actual implementation of the functions prescribed by the functional interfaces depend on the problem the container is trying to solve, all these containers can be pointed with of (which is just an easy way of creating container instances in a functional manner), they all can be mapped, they all can be chained and joined and so on. Their purpose is to contain and make function application standardized, uniform, predictable and clean.

In the last part of the series, I will publish all the remaining code samples, and will walk through the implementation of a minimalist web framework similar to Ruby’s Sinatra, but featuring all the purely functional goodness we discussed so far, so stay tuned!

--

--

Viktor Daróczi
Viktor Daróczi

Written by Viktor Daróczi

A software engineer focusing on the fun part of computing.

No responses yet