why should one prefer call_user_func_array over regular calling of function?

前端 未结 6 2046
轮回少年
轮回少年 2020-12-12 20:41
function foobar($arg, $arg2) {
    echo __FUNCTION__, \" got $arg and $arg2\\n\";
}
foobar(\'one\',\'two\'); // OUTPUTS : foobar got one and two 

call_user_func_arr         


        
6条回答
  •  隐瞒了意图╮
    2020-12-12 21:25

    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:

    • Clearly it's very tedious to write and call functions in this way.
    • If we provide curried functions, they'll be awkward whenever their 'memory' ability isn't needed.
    • If we rely on the 'memory' ability of curried functions, we'll be disappointed when other people's code doesn't provide it.

    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
    

提交回复
热议问题