====== PHP RFC: Syntax for variadic functions ====== * Date: 2013-08-27 * Author: Nikita Popov * Status: Implemented in PHP 5.6 ([[https://github.com/php/php-src/commit/0d7a6388663b76ebed6585ac92dfca5ef65fa7af|git:0d7a6388]]) * Patch: https://github.com/php/php-src/pull/421 * Mailing list discussion: http://markmail.org/message/uhewgv7zaagkgzdv ===== Proposal ===== ==== Introduction ==== Currently variadic functions are implemented by fetching the function arguments using ''func_get_args()''. The following code sample shows an implementation of a variadic function used to prepare and execute a MySQL query (I'll be making use of this example throughout the RFC): class MySQL implements DB { protected $pdo; public function query($query) { $stmt = $this->pdo->prepare($query); $stmt->execute(array_slice(func_get_args(), 1)); return $stmt; } // ... } $userData = $db->query('SELECT * FROM users WHERE id = ?', $userID)->fetch(); There are two issues with the above approach: Firstly, by just looking at the function signature ''public function query($query)'' you cannot know that this is actually a variadic function. You'd think that the function can only run a normal query and doesn't support bound parameters. Secondly, because ''func_get_args()'' returns *all* arguments passed to the function you first need to remove the ''$query'' parameter using ''array_slice(func_get_args(), 1)''. This RFC proposed to solve these issues by adding a special syntax for variadic functions: class MySQL implements DB { public function query($query, ...$params) { $stmt = $this->pdo->prepare($query); $stmt->execute($params); return $stmt; } // ... } $userData = $db->query('SELECT * FROM users WHERE id = ?', $userID)->fetch(); The ''%%...$params%%'' syntax indicates that this is a variadic function and that all arguments after ''$query'' should be put into the ''$params'' array. Using the new syntax both of the issues mentioned above are solved. ==== Population of variadic parameter ==== The following example shows how the variadic parameter ''%%...$params%%'' is populated depending on the number of passed arguments: function fn($reqParam, $optParam = null, ...$params) { var_dump($reqParam, $optParam, $params); } fn(1); // 1, null, [] fn(1, 2); // 1, 2, [] fn(1, 2, 3); // 1, 2, [3] fn(1, 2, 3, 4); // 1, 2, [3, 4] fn(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5] ''$params'' will be an empty array if the number of passed arguments is smaller than the number of declared parameters. Any further arguments will be added to the ''$params'' array (in the order in which they were passed). The ''$params'' array is using continuous zero-based indices. ==== By-reference capture ==== The new syntax additionally adds support for capturing variadic arguments by-reference, something that was previously not possible in userland code. Only internal functions could make use of this via ''ZEND_ACC_PASS_REST_BY_REF''. This would allow implementing functions like ''sscanf()'' or ''mysqli_stmt::bind_param()'' in userland. The following method uses the new syntax to prepare a query and bind parameters by-reference to it: class MySQL implements DB { public function prepare($query, &...$params) { $stmt = $this->pdo->prepare($query); foreach ($params as $i => &$param) { $stmt->bindParam($i + 1, $param); } return $stmt; } // ... } $stmt = $db->prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)', $name, $email, $age); foreach ($usersToInsert as list($name, $email, $age)) { $stmt->execute(); } A by-reference capture of variadic arguments is indicated by a ''&'' before the ellipsis ''%%...%%''. ==== Type hints ==== Furthermore it's possible to provide a typehint that all variadic arguments are checked against. E.g. this is how the signature of ''array_merge'' implemented in userland would look like: function array_merge(array ...$arrays) { /* ... */ } PHP would make sure that all arguments are actually arrays. This also works for all other typehints like ''%%callable ...$callbacks%%'' or ''%%Route ...$routes%%''. ==== Prototype checks ==== An advantage of declaring variadics in the parameter list is that the signature can be enforced by intefaces and during inheritance. The ''MySQL::query()'' and ''MySQL::prepare()'' examples above already referenced an interface ''DB''. This is how the interface could look like: interface DB { public function query($query, ...$params); public function prepare($query, &...$params); // ... } This interface will force any implementation to make both these functions variadic and will also enforce the by-ref capture for ''prepare()''. The exact protoype checks (what is valid and what is not) are outlined below: // INVALID: Turning a variadic function into a non-variadic one public function query($query, ...$params) public function query($query) // VALID: Turning a non-variadic function into a variadic one public function query($query) public function query($query, ...$params) // Note: This is allowed as ...$params is optional and PHP allows additional optional arguments // INVALID: Changing the passing mode for variadic parameters public function query($query, &...$params) public function query($query, ...$params) // INVALID: Changing the typehint of a variadic parameter to an incompatible typehint public function query($query, array ...$params) public function query($query, callable ...$params) // INVALID: Removing parameter before the variadic parameter public function query($query, ...$params) public function query(...$params) // Note: Personally I don't think this makes sense, but this is how // PHP behaves in general, so I'm staying consistent with it // VALID: Adding additional optional parameter before the variadic parameter (with compatible typehint) public function query($query, array ...$params) public function query($query, array $extraParam = null, array ...$params) // INVALID: Adding additional optional parameter with incompatible typehint public function query($query, array ...$params) public function query($query, callable $extraParam = null, array ...$params) ==== Syntactic restrictions ==== There may be only one variadic parameter and it needs to be the last parameter of the function. A variadic parameter may not have a default value. As such all of the following are invalid: function fn(...$args, $arg) function fn(...$args1, ...$args2) function fn($arg, ...$args = []) ==== Reflection ==== Two new methods are added to Reflection: bool ReflectionFunction::isVariadic() bool ReflectionParameter::isVariadic() The functions will return ''true'' if the function/parameter is variadic, ''false'' otherwise. ==== Summary ==== To sum up, the feature adds the following new syntax: * ''%%function fn($arg, ...$args)%%'': Capture all variadic arguments into the ''$args'' array * ''%%function fn($arg, &...$args)%%'': Do the capture by reference * ''%%function fn($arg, array ...$args)%%'': Enforce that all variadic arguments are arrays (or some other typehint) * ''%%function fn($arg, array &...$args)%%'': Combine both - variadic arguments are arrays that are captured by-reference The advantages of the syntax are: * It's immidiately clear that a function is variadic, without having to read documentation. * It is no longer necessary to ''array_slice()'' the variadic arguments from ''func_get_args()'' * It is now possible to do variadic by-reference captures * Types can be checked with a typehint (rather than a manual loop) * Variadic prototypes can be enforce in interfaces / by inheritance ===== Backwards compatibility ===== ==== Userland ==== This change does not break backwards compatibility for userland code. In particular, this RFC does not propose to deprecate or remove the ''func_get_args()'' family of functions, at least not any time soon. ==== Internal ==== The ''pass_rest_by_ref'' argument of ''ZEND_BEGIN_ARG_INFO'' and ''ZEND_BEING_ARG_INFO_EX'' is no longer used. Instead functions can declare a variadic argument in the arginfo using ''ZEND_ARG_VARIADIC_INFO''. For example, this is how the arginfo for ''sscanf()'' changed: // OLD: ZEND_BEGIN_ARG_INFO_EX(arginfo_sscanf, 1, 0, 2) ZEND_ARG_INFO(0, str) ZEND_ARG_INFO(0, format) ZEND_ARG_INFO(1, ...) ZEND_END_ARG_INFO() // NEW: ZEND_BEGIN_ARG_INFO_EX(arginfo_sscanf, 0, 0, 2) ZEND_ARG_INFO(0, str) ZEND_ARG_INFO(0, format) ZEND_ARG_VARIADIC_INFO(1, vars) ZEND_END_ARG_INFO() It would theoretically be possible to retain support for ''pass_rest_by_ref'' (by automatically generating a variadic arg), but as this is an exceedingly rarely used feature I don't think this is necessary. All usages of it in php-src have been replaced. Apart from this the change should be transparent from an internals point of view. Macros like ''ARG_MUST_BE_SENT_BY_REF'' continue to work. ===== Discussion ===== ==== Choice of syntax ==== Presumably this RFC will quickly deteriorate towards a bike-shedding of the best syntax for variadic parameters, so I'll try to explain my choice for the proposed syntax right away: The use of ''%%...%%'' is already familiar from the PHP documentation, where variadics are denoted using a trailing ''%%$...%%'' parameter. The reason ''%%...%%'' follows *before* the parameter in the proposed syntax is to clearly show that the typehint/ref-modifier before it applies to all arguments. Some possible alternative syntax and why I don't like it: * ''%%$args...%%''. With ref-modifier (''%%&$args...%%'') this does not show well that the individual arguments are references, rather than ''$args'' itself. With typehint (''%%array $args...%%'') it also looks like the typehint applies to ''$args'' itself rather than all variadic arguments. * ''*$args''. This is the syntax that both Ruby and Python use. For PHP this does not work well because ''*$'' is a weird combination. It gets worse with a by-reference capture: ''&*$args''. This looks like a random sequences of special characters. Combined with a typehint the syntax looks a lot like a pointer: ''Foo *$args''. * ''params $args''. This is what C# does. This would require making ''params'' a keyword. Furthermore this doesn't have any nice way to declare typehints. In C# this is done using ''params type[] args'', but PHP doesn't have ''type[]'' hints and introducing them only here doesn't seem right. The proposed syntax is also used by Java and will be used in [[http://wiki.ecmascript.org/doku.php?id=harmony:rest_parameters|Javascript (ECMAScript Harmony proposal)]]. Go and C++ also employ a similar syntax. ===== Patch ===== Patch available in PR#421: https://github.com/php/php-src/pull/421 ===== Vote ===== The vote started on 16.09.2013 and ended on 23.09.2013. There were 36 votes in favor and one against, as such the necessary two-third majority is met and this feature is **accepted**. * Yes * No ===== Argument unpacking ===== The [[rfc:argument_unpacking|argument unpacking RFC]] introduces the following related syntax: $db->query($query, ...$params);