Functional Programming for WordPress Developers, Part 3
Parts one and two of this series discussed how modern PHP treats functions as first class data, and how functions are the main focus in functional programming. And, I showed how a WordPress plugin can be built by just partially applying parameters and combining functions together to make new functions.
These programming techniques are neat, but code is useless without data to work on. Functional programmers like to talk about Monads, a group of tools for managing data, or state, in functional code.
Ignore the classic definitions of monads, things like “a monad is a monoid in the category of endofunctors.” It didn’t take long for that to go from academic lingo to a glib in-joke. Instead, think of a monad as a stripped-down object, one that only cares about a single piece of private data. There’s usally only two methods: value() to retrieve the value, and map(). There’s no other member variables. A monad is a data burrito.
Through these limitations, monads force you to keep your data and functionality separate.
The most basic monad is the Just monad. It just wraps a value. It doesn’t provide any context for it, and it doesn’t implement any special behavior.
Monads’ map() methods take a function as their only parameter. map() passes that function the monad’s content, and wraps the function’s result in a new monad before returning it. This keeps the original monad & its value immutable, unchanging.
The importance of immutability
Functional programmers despise side-effects. If a function is able to change something outside itself, then it’s not mathematically pure. It also increases the complexity of the program’s logic, leaving you second-guessing how a value was computed.
Consider the following code:
PHP passes f() a reference to $obj, and f() is free to do whatever it wants to with it. We’d expect the program’s output to be (1 + 2) + (1 + 3) = 7. But, in this example, f() adds 5 to the object’s value before it gets passed to g(), making the program output 11!
What if $myObj’s value was immutable, wrapped up in a monad so f() couldn’t change it?
In this example, f() overwrites $value with a new value, but who cares? f() only has a copy of the value, not the monad that called it.
Error prevention and detection
My code samples so far haven’t checked for a single error. But even the most mathematically-pure code needs to deal with the unexpected. Traditionally, programmers signal errors or invalid states with null values. Lots of PHP’s built-in functions do it. The WordPress API even has a _return_null function for a clever shortcut when hooking into filters.
Code ends up littered with checks for nulls. We have to do it, or we risk fatal errors when we try to do anything to one of them.
The Maybe monad maps functions to its value, like the Just monad, as long as its value isn’t null. It returns another Maybe monad with either the new value or another null.
Since Maybe::map() doesn’t call the function you pass if the value is null, your code doesn’t need to check for a null value on its own.
Sometimes, a null is a legitimate value to return. So what do you use to signal an error then? false can be a legitimate return value, too. Something crazy like PHP_INT_MAX? WordPress has the WP_Error class, but you still need to check for errors with is_wp_error() before assuming you got a value.
Either monads let functional programmers defer error checking until they’re ready. Such a monad can be “either” a Left monad, representing an error, or a Right monad standing for success. If you can’t keep them straight, remember the Latin word for “on the left side” is sinister, and we left-handed folk have been grumpy about it for centuries.
It doesn’t matter what value is wrapped in a Left monad; the fact it’s a Left monad tells you something bad happened. It’s the end of the road. Calling Left::map() returns that same Left monad back to you. It doesn’t even bother calling your function on its value. Your code can keep mapping functions assuming everything is fine until it’s time to react and display an error message.