As developers, we're all constantly searching for the cleanest way to write something. Writing that elegant one-liner or refactoring something ugly gives us a deep sense of satisfaction. Unfortunately, working with Drupal forces us to deal with deeply nested arrays that encourage ugly, cumbersome code. It's just too easy to reach for a trusty
There's another way though... With some simple concepts from functional programming, we can avoid those ugly, deeply-nested loops and conditionals that we often see in the Drupal ecosystem. Those same patterns can also help us think about our program more rationally and break a problem down into smaller pieces. This leads to cleaner, more reason-able code.
array_reduce() and array_filter()
The
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
where
mixed callback ( mixed $carry , mixed $item )
Imagine you want to get a sum of all positive values in a nested array. That array might look like this:
$my_array = array( 0 => array( 'name' => 'foo', 'value' => 5, ), 1 => array( 'name' => 'bar', 'value' => -10, ), 2 => array( 'name' => 'baz', 'value' => 15, ), 3 => array( 'name' => 'zap', 'value' => 20, ), );
With a
$sum = 0; foreach ($my_array as $item) { $sum = ($item['value'] > 0) ? $sum + $item['value'] : $sum; } // $sum == 40
With
$sum_values = function ($carry, $item) { return ($item['value'] > 0) ? $carry + $item['value'] : $carry; }; $sum = array_reduce($my_array, $sum_values, 0); // $sum == 40
For every item in the array, the anonymous function gets passed the result of the previous iteration. On the first item, it will be passed
In this trivial example, its advantages are hard to see. A
This functional concept lets your mind reason more clearly about the problem at hand. When writing any logic, one must think "What do I know at this moment and what information must I return?" This constraint often helps me see the problem in a new, simpler way.
Imagine that you wanted to exclude any items named 'foo' from your sum. Having written a
$sum = 0; foreach ($my_array as $item) { if ($item['name'] != 'foo') { $sum = ($item['value'] > 0) ? $sum + $item['value'] : $sum; } } // $sum == 35
Doing things this way forces us to layer on logic, creating exponentially more possible code paths every time we add a rule. Instead, by using anonymous functions,
array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )
It’s callback is even simpler than
Putting everything together, we can define a filtering function and a reduction function independently of one another and then compose them to achieve the same result that we did with the nested loop and conditional. That might look something like this:
$remove_foos = function ($name) { return ($name != 'foo'); }; $sum_values = function ($carry, $item) { return ($item['value'] > 0) ? $carry + $item['value'] : $carry; }; $sum = array_reduce(array_filter($my_array, $remove_foos), $sum_values, 0); // $sum == 35
Using this strategy, each operation is made apparent and separate. First, 'foo's are filtered out, then positives are summed. That's a heck of a lot simpler to grasp later. As you add more rules, think about creating simple, focused functions that can be put together, or composed. By doing so, you’ll be able to reuse a lot more of these filters and reducers and recognize patterns more easily.