rfc:argument_unpacking
Differences
This shows you the differences between two versions of the page.
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)