Concept: built-in functions for common functional primitives(composition, partial application, etc.)

  100739
September 21, 2017 19:14 ajf@ajf.me (Andrea Faulds)
Hi everyone,

Something I've long wanted in PHP has been built-in versions of some 
common functional primitives, functions that operate on functions and 
create new functions for you. I've finally gotten round to bringing this 
up because my operator functions RFC could really benefit from them.

To be specific in what I mean by “functional primitives”, here are some 
examples:

Function composition: a function that takes a function f($x) and a 
function g($y) and returns a new function($x) { return g(f($x)); }. Thus 
compose("trim", "strtoupper") returns a function that strips whitespace 
from and capitalises a string. In practice it would make sense if f() 
could any number of arguments.

Partial application: a function that takes a function and a set of 
arguments, and returns a new function that calls that function with 
those arguments, but also with any additional arguments passed. Thus, to 
take an example from my operator functions RFC, papply("*", 2) returns a 
function that multiplies a number by two. A question here is how to 
decide the positions of the arguments: can I partially apply, say, 
intdiv() and set the second argument, then have the resulting function 
be called with intdiv()'s first argument? Maybe it could take an array 
specifying argument indicies, so papply("/", [1 => 2]) returns a 
function that divides *by* two.

Currying: a function that takes a function and returns a new function 
that takes the function's first parameter and returns another function 
to take its second, which returns another function, and so on, until 
eventually all the function's arguments have been taken and the 
function's result is returned. So, curry("intdiv")(6)(2) is equivalent 
to intdiv(6, 2), and curry("intdiv")(6) is equivalent to 
papply("intdiv", 6).

Reversing the order of the arguments: a function that takes a function 
and returns a new function that calls the function with whatever 
arguments it is passed, but in reverse order. So, reverse("intdiv")(4, 
2) behaves like intdiv(2, 4).

The identity function: returns its argument. This function essentially 
does nothing, but that's exactly the point, it can go in place of a 
callback that could do something more sophisticated.

Constant function: a function that takes a value and returns a function 
that returns that value. Like the identity function, the function this 
returns is useless when called directly, but can have some use as a 
callback.

This might not be an exhaustive list. There might be some other 
functions of this kind that make sense, and suggestions would be 
appreciated. Note that PHP has some functional primitives already 
(array_map, array_reduce), so in a sense I'm just looking for what it's 
missing.

If these functions were to be added, most of them would make sense as 
both global functions and methods on Closure (think mysqli_* vs 
$mysqli->*). The former makes sense when using callables which aren't 
(neccessarily) closures, e.g. when composing built-in PHP functions 
together. Of course, they could be just methods on Closure and have no 
global function counterparts, but I think compose("trim", "strtoupper") 
is much more appealing in practice than 
Closure::fromCallable("trim")->compose(Closure::fromCallable("strtoupper")). 
At that point you might as well just write the composition yourself as a 
closure.

I'd like to acknowledge that of course almost all of these can be easily 
written in plain PHP as userland functions, and indeed have been. 
However, if PHP includes common operations like these in its standard 
library, it increases the base language's expressive power. It also 
means we can provide faster and edge-case-complete versions. For 
example, while a basic implementation of function composition is five 
lines or so of PHP code, a version that handles reflection and 
references correctly is significantly longer and slower. If PHP has this 
as a built-in function, it can be implemented more efficiently. This is 
particularly important given this kind of higher-order function is often 
used with operations like array_map() which have a multiplicative effect 
that can make their speed significant.

Please tell me your thoughts on this idea.

Thanks!

-- 
Andrea Faulds
https://ajf.me/