rfc:argument_unpacking

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

rfc:argument_unpacking [2014/01/11 11:19]
nikic
rfc:argument_unpacking [2017/09/22 13:28]
Line 1: Line 1:
-====== PHP RFC: Argument Unpacking ====== 
-  * Date: 2013-08-30 
-  * Author: Nikita Popov <nikic@php.net> 
-  * Status: Accepted 
-  * Proposed for: PHP 5.6 
-  * Patch: https://github.com/php/php-src/pull/477 
-  * Mailing list discussion: http://markmail.org/message/dxae5ybjldg6pftp 
  
-===== Introduction ===== 
- 
-This RFC complements the [[rfc:variadics|variadics RFC]]. It introduces a syntax for unpacking arrays and Traversables into argument lists (also known as "splat operator", "scatter operator" or "spread operator"). 
- 
-As a usage example, consider a variadic method ''%%public function query($query, ...$params)%%''. You are provided a ''$query'' and an array of ''$params'' and want to call the method using these. Currently this is possible using ''call_user_func_array()'': 
- 
-<code php> 
-call_user_func_array([$db, 'query'], array_merge(array($query), $params)); 
-</code> 
- 
-This RFC proposes a syntax for unpacking arguments directly in the call syntax: 
- 
-<code php> 
-$db->query($query, ...$params); 
-</code> 
- 
-===== Proposal ===== 
- 
-An argument in a function call that is prefixed by ''%%...%%'' will be "unpacked": Instead of passing the argument itself to the function the elements it contains will be passed (as individual arguments). This works both for arrays and Traversables. 
- 
-As such all of the following function calls are equivalent: 
- 
-<code php> 
-function test(...$args) { var_dump($args); } 
- 
-test(1, 2, 3);                         // [1, 2, 3] 
-test(...[1, 2, 3]);                    // [1, 2, 3] 
-test(...new ArrayIterator([1, 2, 3])); // [1, 2, 3] 
- 
-// Note: It doesn't really make sense to unpack a constant array like [1, 2, 3]. 
-//       Normally these would unpack some variable like ...$args 
-</code> 
- 
-It's possible to use ''%%...%%'' multiple times in a call and it is possible to mix it with normal arguments: 
- 
-<code php> 
-$args1 = [1, 2, 3]; 
-$args2 = [4, 5, 6]; 
-test(...$args1, ...$args2); // [1, 2, 3, 4, 5, 6] 
-test(1, 2, 3, ...$args2);   // [1, 2, 3, 4, 5, 6] 
-test(...$args1, 4, 5, 6);   // [1, 2, 3, 4, 5, 6] 
-</code> 
- 
-The ''%%...%%'' operator works in all argument lists, including ''new'' expressions: 
- 
-<code php> 
-fn(...$args); 
-$fn(...$args); 
-$obj->fn(...$args); 
-ClassName::fn(...$args); 
-new ClassName(...$args); 
-</code> 
- 
-Argument unpacking is not limited to variadic functions, it can also be used on "normal" functions: 
- 
-<code php> 
-function test($arg1, $arg2, $arg3 = null) { 
-    var_dump($arg1, $arg2, $arg3); 
-} 
- 
-test(...[1, 2]);       // 1, 2 
-test(...[1, 2, 3]);    // 1, 2, 3 
-test(...[1, 2, 3, 4]); // 1, 2, 3 (remaining arg is not captured by the function declaration) 
-</code> 
- 
-If you try to unpack something that is not an array or Traversable a warning is thrown, but apart from that the call continues as usual: 
- 
-<code php> 
-var_dump(1, 2, ...null, 3, 4); 
-// Warning: Only arrays and Traversables can be unpacked 
-// int(1) int(2) int(3) int(4) 
-</code> 
- 
-==== By-reference passing ==== 
- 
-If an array is unpacked the elements will by passed by-value/by-reference according to the function definition: 
- 
-<code php> 
-function test($val1, $val2, &...$refs) { 
-    foreach ($refs as &$ref) ++$ref; 
-} 
- 
-$array = [1, 2, 3, 4, 5]; 
-test(...$array); 
-var_dump($array); // [1, 2, 4, 5, 6] 
-</code> 
- 
-By-reference passing will not work if the unpacked entity is a Traversable. Instead an ''E_WARNING'' level error is thrown and the argument is passed by-value instead: 
- 
-<code php> 
-test(...new ArrayIterator([1, 2, 3, 4, 5])); 
-// Warning: Cannot pass by-reference argument 3 of test() by unpacking a Traversable, passing by-value instead 
-</code> 
- 
-The reasons why we can't pass by-reference from a Traversable are two-fold:  
- 
-  * It's not possible to determine the number of elements in a Traversable ahead of time. As such we can not know whether unpacking the Traversable will or will not hit a by-reference argument. 
-  * It's not possible to determine if a Traversable has support for by-reference iteration or if it will trigger an error if this is requested. 
- 
-==== String keys ==== 
- 
-In order to ensure forward-compatibility with [[rfc:named_params|named arguments]] the unpacking operator does not support string keys. If a string key is encountered during unpacking a recoverable error is thrown. If the error is ignored using a custom error handler, no further arguments will be unpacked but the call still happens. 
- 
-===== Backward Compatibility ===== 
- 
-This change does not break userland or internal compatibility. 
- 
-===== Advantages over call_user_func_array ===== 
- 
-Usage of ''call_user_func_array'' becomes complicated if you need to pass fixed arguments as well. Compare: 
- 
-<code php> 
-call_user_func_array([$db, 'query'], array_merge(array($query), $params)); 
-// vs 
-$db->query($query, ...$params); 
-</code> 
- 
-''call_user_func_array'' requires a callback. So even if the called function/method is known, you still need to use a dynamic string/array callback. This usually precludes any IDE support. 
- 
-''call_user_func_array'' does not work for constructors. Instead ''ReflectionClass::newInstanceArgs()'' has to be used: 
- 
-<code php> 
-(new ReflectionClass('ClassName'))->newInstanceArgs($args); 
-// vs 
-new ClassName(...$args); 
-</code> 
- 
-Futhermore ''call_user_func_array'' has a rather large performance impact. If a large number of calls go through it, this can make a signficant difference. For this reason projects ((I've seen this used at least in Laravel and Drupal and a bunch of other code)) often replace particularly common ''call_user_func_array'' calls with a switch statement of the following form: 
- 
-<code php> 
-switch (count($args)) { 
-    case 0: $func(); break; 
-    case 1: $func($args[0]); break; 
-    case 2: $func($args[0], $args[1]); break; 
-    case 3: $func($args[0], $args[1], $args[2]); break; 
-    case 4: $func($args[0], $args[1], $args[2], $args[3]); break; 
-    case 5: $func($args[0], $args[1], $args[2], $args[3], $args[4]); break; 
-    default: call_user_func_array($func, $args); break; 
-} 
-</code> 
- 
-The ''%%...%%'' argument unpacking syntax is about 3.5 to 4 times faster than ''call_user_func_args''. This solves the performance issue. [[https://gist.github.com/nikic/6390366|Benchmark code and results]]. 
- 
-Lastly, it seems that people naturally expect that this syntax is present if the variadics syntax is present. So if we implement variadics, it's probably best to include this as well. 
- 
-===== Examples ===== 
- 
-The code samples in the "Proposal" section are rather technical and not code you would actually write. This section contains a few more practical examples of this feature. 
- 
-==== Extending variadic functions: forwarding ==== 
- 
-The introduction already mentioned ''%%$db->query($query, ...$params)%%'' as an example. At this point you could wonder: Why would I want to write code like that? Why should I have the parameters only as an array? 
- 
-One case where this occurs is when extending variadic functions: 
- 
-<code php> 
-class MySqlWithLogging extends MySql { 
-    protected $logger; 
-    public function query($query, ...$params) { 
-        $this->logger->log( 
-            'Running query "%s" with parameters [%s]', 
-            $query, implode(', ', $params) 
-        ); 
-         
-        return parent::query($query, ...$params); 
-    } 
-} 
-</code> 
- 
-The above code sample extends the variadic ''query()'' method with logging and needs to forward all arguments to the parent function. 
- 
-==== Partial application: multiple unpacks ==== 
- 
-Some people were wondering on what occasion you would ever want to unpack *two* arguments in one function call. An example of such a usage is "partial application". 
- 
-If you are not familiar with the concept, partial application allows you to "bind" arguments to a function: 
- 
-<code php> 
-$arrayToLower = bind('array_map', 'strtolower'); 
- 
-$arrayToLower(['Foo', 'BAR', 'baZ']); // returns ['foo', 'bar', 'baz'] 
- 
-// The above $arrayToLower call resolves to: 
-// array_map('strtolower', ['Foo', 'BAR', 'baZ']) 
-</code> 
- 
-This is a common functional paradigm, but rather rarely used in PHP. Anyway, an "old-style" (no variadic syntax, no argument unpacking) definition of the ''bind()'' function would look like this: 
- 
-<code php> 
-function bind(callable $function) { 
-    $boundArgs = array_slice(func_get_args(), 1); 
-    return function() use ($function, $boundArgs) { 
-        return call_user_func_array( 
-            $function, array_merge($boundArgs, func_get_args()) 
-        ); 
-    } 
-} 
-</code> 
- 
-And the "new-style" definition (with variadic syntax and argument unpacking) looks like this: 
- 
-<code php> 
-function bind(callable $function, ...$boundArgs) { 
-    return function(...$args) use($function, $boundArgs) { 
-        return $function(...$boundArgs, ...$args); 
-    } 
-} 
-</code> 
- 
-==== Normal arguments after unpacked arguments ==== 
- 
-Some people also had doubts regarding the usefulness of allowing ''%%foo(...$args, $arg)%%'' calls, where a normal argument follows after an unpacked argument. 
- 
-One use-case for this pattern are the variants of ''array_diff'' and ''array_intersect'' which accept callback functions as the **last** one or two arguments: 
- 
-<code php> 
-array_udiff(...$arrays, $valueCmpFunction); 
-array_udiff_uassoc(...$arrays, $valueCmpFunction, $keyCmpFunction); 
-</code> 
- 
-While this kind of API was undoubtedly a bad choice in the first place, I see little reason not to support calls to them. 
- 
-Another use case is appending one additional argument to a variadic argument list. For example, the following snippet adds an additional ''LIMIT'' parameter to an existing query: 
- 
-<code php> 
-$this->query($query . ' LIMIT ?', ...$params, $limit); 
-</code> 
- 
-===== Patch ===== 
- 
-The patch for this features is available as a PR: https://github.com/php/php-src/pull/477 
- 
-===== Vote ===== 
- 
-As this is a language change a two third majority is required. 
- 
-Voting started 2013-12-21 and ended 2014-01-11. 
- 
-<doodle title="Implement argument unpacking in PHP 5.6?" auth="nikic" voteType="single" closed="true"> 
-   * Yes 
-   * No 
-</doodle> 
- 
-===== Support in other languages ===== 
- 
-This feature is supported by many languages. Some of the more important ones being: 
- 
-  * [[http://docs.python.org/2/tutorial/controlflow.html#unpacking-argument-lists|Python]] using the ''*args'' syntax 
-  * [[http://endofline.wordpress.com/2011/01/21/the-strange-ruby-splat/#calling_methods|Ruby]] using Python's syntax 
-  * Java supports this, but only for variadic parameters and without any special syntax (type based) 
-  * JavaScript ([[http://wiki.ecmascript.org/doku.php?id=harmony:spread|ECMAScript Harmony]]) using the same syntax proposed here 
rfc/argument_unpacking.txt · Last modified: 2017/09/22 13:28 (external edit)