function foobar($arg, $arg2) {
echo __FUNCTION__, \" got $arg and $arg2\\n\";
}
foobar(\'one\',\'two\'); // OUTPUTS : foobar got one and two
call_user_func_arr
call_user_func_array performs "uncurrying", which is the opposite of "currying".
The following applies to all of PHP's "callables" (named functions, closures, methods, __invoke, etc.), so for simplicity let's ignore the differences and just focus on closures.
If we want to accept multiple arguments, PHP lets us do that with 3 different APIs. The usual way is this:
$usual = function($a, $b, $c, $d) {
return $a + $b + $c + $d;
};
$result = $usual(10, 20, 30, 40); // $result == 100
Another way is called curried form:
$curried = function($a) {
return function($b) use ($a) {
return function($c) use ($a, $b) {
return function($d) use ($a, $b, $c) {
return $a + $b + $c + $d;
};
};
};
};
$result = call_user_func(
call_user_func(
call_user_func(
$curried(10),
20),
30),
40); // $result == 100
The advantage is that all curried functions can be called in the same way: give them one argument.
If more arguments are required, more curried functions are returned, which 'remember' the previous arguments. This allows us to pass in some arguments now and the rest later.
There are some problems with this:
We can fix all of these issues by using a conversion function (disclaimer: that's my blog). This lets us write and call our functions in the usual way, but gives them the same 'memory' ability as if they were curried:
$curried = curry(function($a, $b, $c, $d) {
return $a + $b + $c + $d;
});
$result1 = $curried(10, 20, 30, 40); // $result1 = 100
$result2 = call_user_func($curried(10, 20), 30, 40); // $result2 = 100
The third way is called uncurried and takes all of its arguments in one:
$uncurried = function($args) {
return $args[0] + $args[1] + $args[2] + $args[3];
};
$result = $uncurried([10, 20, 30, 40]); // $result == 100
Just like with curried functions, uncurried functions can all be called with one argument, although this time it's an array. We still face the same compatibility problems as curried functions: if we choose to use uncurried functions, we can't rely on everyone else choosing the same. Hence we also need a conversion function for uncurrying. That's what call_user_func_array does:
$uncurried = function($args) use ($usual) {
return call_user_func_array($usual, $args);
};
$result1 = $usual(10, 20, 30, 40); // $result1 = 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 = 100
Interestingly, we can get rid of that extra function($args) wrapper (a process known as "eta-reduction") by currying call_user_func_array:
$uncurried = curry('call_user_func_array', $usual);
$result = $uncurried([10, 20, 30, 40]); // $result == 100
Unfortunately call_user_func_array isn't as smart as curry; it won't automatically convert between the two. We can write our own uncurry function which has that ability:
function uncurry($f)
{
return function($args) use ($f) {
return call_user_func_array(
$f,
(count(func_get_args()) > 1)? func_get_args()
: $args);
};
}
$uncurried = uncurry($usual);
$result1 = $uncurried(10, 20, 30, 40); // $result1 == 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 == 100
These conversion functions show that PHP's "usual" way of defining functions is actually redundant: if we replaced PHP's "usual" functions with 'smart' curried or uncurried ones, lots of code would carry on working. If we did that, it's better to curry everything and selectively uncurry as needed, since that's easier than going the other way around.
Unfortunately, some things which expect a variable number of arguments using func_get_args would break, as well as functions with default argument values.
Interestingly, default values are just a special form of currying. We could mostly do without them if we put those arguments first instead of last, and provided a bunch of alternative definitions which curry in the defaults. For example:
$defaults = function($a, $b, $c = 30, $d = 40) {
return $a + $b + $c + $d;
};
$def1 = $defaults(10, 20, 30, 40); // $def1 == 100
$def2 = $defaults(10, 20, 30); // $def2 == 100
$def3 = $defaults(10, 20); // $def3 == 100
$curried = function($d, $c, $a, $b) {
return $a + $b + $c + $d;
};
$curriedD = $curried(40);
$curriedDC = $curriedD(30);
$cur1 = $curried(10, 20, 30, 40); // $cur1 == 100
$cur2 = $curriedD(10, 20, 30); // $cur2 == 100
$cur3 = $curriedDC(10, 20); // $cur3 == 100