Functional composition in PHP

I was looking at a particular line of code that mabismad wrote in an answer to another thread.

$links = array_filter(array_unique(array_merge([1],range($page-$range, $page+$range),[$total_pages])),
function ($val) use ($total_pages) { return $val >= 1 && $val <= $total_pages; });

If expanded it is a bit easier to see what is going on.

$links = array_filter(
  array_unique(
    array_merge(
      [1],
      range($page-$range, $page+$range),
      [$total_pages]
    )
  ),
  function ($val) use ($total_pages) { return $val >= 1 && $val <= $total_pages; }
);

I have been doing quite a bit with function composition in Javascript, and this piece of code got me wondering about how to apply this in PHP. My knowledge of PHP is pretty basic, so there was a lot of trial and error.

Composing with Pipe

The first function I wanted to look at making was pipe, and I came up with this.

function pipe($fn, ...$fns) {
  return function (...$args) use($fn, $fns) {
    return array_reduce(
      $fns, 
      fn($g, $f) => $f($g),
      $fn(...$args)
    );
  };
}

The function takes a variable number of functions as arguments, which are stored returning an inner function.

The returned inner function takes a variable number of arguments which are passed to the first of the stored functions, then in sequence the output of each function is passed on to the next function.

Example

$add = fn($x, $y) => $x + $y;
$increment = fn($x) => $x + 1;
$double = fn($x) => $x * 2;

$calc = pipe($add, $increment, $double);

echo $calc(2, 3); // 5 → 6 → 12

Now looking at mabismad’s code again I can see that array_filter would be the last function in the chain. The issue here is that array_filter expects two arguments, so currying was my next thought.

Curry Two Function

This a based on ramdaJS’s implementation, just in a reversed order.

function curry2($fn) {
  return function($x = null, $y = null) use($fn) {
    switch (func_num_args()) {
      case 2:
        return $fn($x, $y);
      case 1:
        return fn($y) => $fn($x, $y);
      case 0:
        return $fn;
    }
  };
}

Example

$add_curried = curry2($add);
$add_ten = $add_curried(10);
echo $add_ten(5); // 15

Great but array_filter takes the callback function as a second argument, and that is the one I want to fix.

One way I tried was to use a wrapper function.

$filter = curry2(fn($fn, $arr) => array_filter($arr, $fn));

$greater_than_5 = $filter(fn($x) => $x > 5));
echo $greater_than_5([1,2,6,7,8]); // [6,7,8]

An alternative approach to this was to create a flip function to automate this process.

function flip($fn) {
    return function($x, $y) use($fn) {
        return $fn($y, $x);
    };
}

The $filter function could now be created like this instead.

$filter = curry2(flip('array_filter');

A bit easier on the eye.

Putting it Altogether

$filter = curry2(flip('array_filter'));

$total_pages = 10;
$page = 2;
$range = 3;

$clamp_pages = $filter(fn($x) => $x >= 1 && $x <= $total_pages);
$filter_pages = pipe('array_merge', 'array_unique', $clamp_pages);

var_dump($filter_pages([1], range($page-$range, $page+$range), [$total_pages]));
// [1, 2, 3, 4, 5, 10]

or with a compose two function instead.

function compose2($f, $g) {
    return function(...$args) use ($f, $g) {
        return $f($g(...$args));
    };
}

$pages = [1, ...range($page-$range, $page+$range), $total_pages];
$clamp_pages = $filter(fn($x) => $x >= 1 && $x <= $total_pages);
// Just composing the two functions here - right to left.
$filter_pages = compose2($clamp_pages, 'array_unique');

var_dump($filter_pages($pages));

Conclusion

As mentioned my PHP knowledge is very basic, so the above may well be utter bunkum.

It was interesting to look at a functional approach to this though — for me anyway. I also learnt a bit of PHP along the way :slight_smile: .

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.