rfc:named_params
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:named_params [2013/09/06 22:15] – nikic | rfc:named_params [2022/05/26 15:31] (current) – Revert previous accidental change iquito | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== PHP RFC: Named Parameters | + | ====== PHP RFC: Named Arguments |
- | * Version: 0.9 | + | * Date: 2013-09-06, significantly updated 2020-05-05 |
- | * Date: 2013-09-06 | + | |
* Author: Nikita Popov < | * Author: Nikita Popov < | ||
- | * Status: | + | * Status: |
- | * Proposed for: PHP 5.6 | + | * Target Version: PHP 8.0 |
+ | * Implementation: | ||
- | ===== State of this RFC ===== | + | ===== Introduction |
- | This is a preliminary RFC for named parameters. It's purpose is to find out if we want to support them in the next PHP version and if so, how the implementation should work. The syntax | + | Named arguments allow passing arguments |
- | The implementation that accompanies this proposal is not complete yet. As this is a very complicated feature I do not wish to spend time finishing it without knowing that we actually want this feature. | + | To give a simple example: |
- | + | ||
- | The implementation is based off and includes the [[rfc: | + | |
- | + | ||
- | ===== What are named arguments? ===== | + | |
- | + | ||
- | Named arguments are a way to pass arguments to a function, which makes use of the parameter names rather than the position of the parameters: | + | |
<code php> | <code php> | ||
// Using positional arguments: | // Using positional arguments: | ||
- | array_fill(0, | + | array_fill(0, |
// Using named arguments: | // Using named arguments: | ||
- | array_fill(start_index | + | array_fill(start_index: 0, num: 100, value: 50); |
</ | </ | ||
Line 28: | Line 23: | ||
<code php> | <code php> | ||
- | array_fill(value | + | array_fill(value: 50, num: 100, start_index: 0); |
</ | </ | ||
- | It is possible to combine named arguments with normal, positional arguments and it is also possible to specify only some of the optional arguments of a function, | + | It is possible to combine named arguments with normal, positional arguments and it is also possible to specify only some of the optional arguments of a function, |
<code php> | <code php> | ||
- | htmlspecialchars($string, | + | htmlspecialchars($string, |
// Same as | // Same as | ||
htmlspecialchars($string, | htmlspecialchars($string, | ||
Line 41: | Line 36: | ||
===== What are the benefits of named arguments? ===== | ===== What are the benefits of named arguments? ===== | ||
- | One obvious benefit of named arguments can be seen in the last code sample (using '' | + | ==== Skipping |
- | This is also possible with the [[rfc: | + | One obvious benefit of named arguments can be seen in the last code sample (using '' |
+ | |||
+ | This is also possible with the [[rfc: | ||
<code php> | <code php> | ||
htmlspecialchars($string, | htmlspecialchars($string, | ||
// vs | // vs | ||
- | htmlspecialchars($string, | + | htmlspecialchars($string, |
</ | </ | ||
- | Seeing the first line you will not know what the '' | + | Seeing the first line you will not know what the '' |
+ | |||
+ | ==== Self-documenting code ==== | ||
- | The benefit of making code self-documenting | + | The benefit of making code self-documenting |
<code php> | <code php> | ||
- | $str-> | + | array_slice($array, $offset, $length, true); |
// vs | // vs | ||
- | $str-> | + | array_slice($array, $offset, $length, preserve_keys: |
</ | </ | ||
- | Currently you can already get something similar to named arguments by taking an '' | + | If I wasn't writing this example right now, I would not know what the fourth parameter of '' |
- | <code php> | + | ==== Object Initialization ==== |
- | htmlspecialchars($string, | + | |
- | </ | + | |
- | Using an '' | + | The [[rfc: |
- | * The available options are not documented in the signature. You have to look into the code to find out. | + | <PHP> |
- | * Handling '' | + | // Part of PHP AST representation |
- | * Something like '' | + | class ParamNode extends Node { |
+ | public function __construct( | ||
+ | public string | ||
+ | public ExprNode $default | ||
+ | | ||
+ | public bool $byRef = false, | ||
+ | public bool $variadic = false, | ||
+ | Location $startLoc = null, | ||
+ | Location $endLoc = null, | ||
+ | ) { | ||
+ | parent:: | ||
+ | } | ||
+ | } | ||
+ | </ | ||
- | Lastly, named arguments allow a new sort of variadic function, one which can take not just an ordered list of values, but also a list of key-value pairs. Sample application is the '' | + | Constructors in particular often have a larger than average number |
- | <code php> | + | There have been multiple attempts to make object construction more ergonomic, such as the [[rfc:object-initializer|Object Initializer RFC]] and the [[rfc:compact-object-property-assignment|COPA RFC]]. However, all such attempts have been declined, as they do not integrate well into the language, due to unfavorable interaction with constructors or non-public properties. |
- | // currently possible: | + | |
- | $db-> | + | |
- | ' | + | |
- | $firstName, $lastName, $minAge | + | |
- | ); | + | |
- | // named args additionally allow: | + | |
- | $db-> | + | |
- | ' | + | |
- | firstName => $firstName, lastName => $lastName, minAge => $minAge | + | |
- | ); | + | |
- | </ | + | |
- | ===== Implementation ===== | + | Named arguments solve the object initialization problem as a side-effect, |
- | ==== Internally ==== | + | < |
+ | new ParamNode(" | ||
+ | // becomes: | ||
+ | new ParamNode(" | ||
- | Named args are internally passed the same way as other arguments | + | new ParamNode($name, null, null, $isVariadic, |
+ | // or was it? | ||
+ | new ParamNode($name, | ||
+ | // becomes | ||
+ | new ParamNode($name, | ||
+ | // or | ||
+ | new ParamNode($name, | ||
+ | // and it no longer matters! | ||
+ | </ | ||
- | ==== Errors ==== | + | The benefit of named arguments for object initialization is on the surface the same as for other functions, it just tends to matter more in practice here. |
- | While it is possible to mix positional | + | ==== Type-safe |
- | <code php> | + | One of the common workarounds for the lack of named arguments, is the use of an options array. The previous example could be rewritten to use an options array as follows: |
- | strpos(haystack => " | + | |
- | // Fatal error: Cannot pass positional arguments after named arguments | + | |
- | </ | + | |
- | If a named argument is not known (a parameter with that name does not exist) and the function is not variadic | + | < |
+ | class ParamNode extends Node { | ||
+ | public string $name; | ||
+ | public ExprNode $default; | ||
+ | public TypeNode $type; | ||
+ | public bool $byRef; | ||
+ | public bool $variadic; | ||
- | <code php> | + | public function __construct(string $name, array $options |
- | strpos(hasytack | + | $this->name = $name; |
- | // Fatal error: Unknown named argument | + | $this->default = $options[' |
- | </code> | + | $this-> |
+ | | ||
+ | $this-> | ||
- | When named arguments are in used, it can happen that the same parameter is set twice. In this case the newer value will overwrite the older one and a warning is thrown: | + | parent:: |
+ | $options[' | ||
+ | $options[' | ||
+ | ); | ||
+ | } | ||
+ | } | ||
- | <code php> | + | // Usage: |
- | function test($a, $b) { var_dump($a, $b); } | + | new ParamNode($name, [' |
+ | new ParamNode($name, [' | ||
+ | </ | ||
- | test(1, 1, a => 2); // 2, 1 | + | While this works, and is already possible today, it has a quite a range of disadvantages: |
- | // Warning: Overwriting already passed parameter 1 ($a) | + | |
- | test(a => 1, b => 1, a => 2); // 2, 1 | + | |
- | // Warning: Overwriting already passed parameter 1 ($a) | + | |
- | </ | + | |
- | ==== Collecting | + | * For constructors in particular, it precludes usage of constructor promotion. |
+ | * The available options are not documented in the signature. You have to look at the implementation or phpdoc to find out what is supported and what types it requires. Phpdoc also provides no universally recognized way to document this. | ||
+ | * The type of the option values is not validated unless manually implemented. In the above example, the types will actually be validated due to the use of property types, but this will not follow usual PHP semantics (e.g. if the class declaration uses strict_types, | ||
+ | * Unless you go out of your way to protect against this, passing of unknown | ||
+ | * Use of an options array requires a specific decision at the time the API is introduced. If you start off without one, but then add additional optional parameters and realize that using an options array would be cleaner, you cannot perform the switch without breaking existing API users. | ||
- | Functions declared | + | Named parameters provide the same functionality |
- | Example | + | ==== Attributes ==== |
+ | |||
+ | The use of named arguments in phpdoc annotations is already wide-spread in the ecosystem. While the [[rfc:attributes_v2|Attributes RFC]] replaces phpdoc annotations with a first-class language feature, it does not provide support for named arguments. This means that existing annotations will have to introduce significant structural changes to migrate to the attribute system. | ||
+ | |||
+ | For example, the Symfony '' | ||
+ | |||
+ | < | ||
+ | /** | ||
+ | * @Route("/ | ||
+ | */ | ||
+ | public function show(int $id) { ... } | ||
+ | |||
+ | // Might become: | ||
+ | |||
+ | << | ||
+ | public function show(int $id) { ... } | ||
+ | </ | ||
+ | |||
+ | Introducing named arguments in the same version as attributes would allow retaining exactly the same structure as before: | ||
+ | |||
+ | < | ||
+ | << | ||
+ | public function show(int $id) { ... } | ||
+ | </ | ||
+ | |||
+ | Some changes would still be necessary due to the lack of support for nested annotations, | ||
+ | |||
+ | ===== Proposal ===== | ||
+ | |||
+ | ==== Syntax ==== | ||
+ | |||
+ | Named arguments are passed by prefixing the value with the parameter name followed by a colon: | ||
+ | |||
+ | < | ||
+ | callAFunction(paramName: | ||
+ | </ | ||
+ | |||
+ | It is possible to use reserved keywords as the parameter name: | ||
+ | |||
+ | < | ||
+ | array_foobar(array: | ||
+ | </ | ||
+ | |||
+ | The parameter name must be an identifier, it's not possible to specify it dynamically: | ||
+ | |||
+ | < | ||
+ | // NOT supported. | ||
+ | function_name($variableStoringParamName: | ||
+ | </ | ||
+ | |||
+ | This syntax is not supported, because it would create an ambiguity: Is '' | ||
+ | |||
+ | Some syntax alternatives that are technically feasible are: | ||
+ | |||
+ | < | ||
+ | function_name(paramName: | ||
+ | function_name(paramName => $value); | ||
+ | function_name(paramName = $value); | ||
+ | function_name(paramName=$value); | ||
+ | function_name($paramName: | ||
+ | function_name($paramName => $value); // (5) | ||
+ | </ | ||
+ | |||
+ | It should be noted that the following syntax is not possible, because it already constitutes legal code: | ||
+ | |||
+ | < | ||
+ | function_name($paramName = $value); | ||
+ | </ | ||
+ | |||
+ | A previous version of this RFC proposed '' | ||
+ | |||
+ | ==== Constraints ==== | ||
+ | |||
+ | It is possible to use positional and named arguments in the same call, however the named arguments must come after the positional arguments: | ||
+ | |||
+ | < | ||
+ | // Legal | ||
+ | test($foo, param: $bar); | ||
+ | // Compile-time error | ||
+ | test(param: $bar, $foo); | ||
+ | </ | ||
+ | |||
+ | Passing the same parameter multiple times results in an '' | ||
+ | |||
+ | < | ||
+ | function test($param) { ... } | ||
+ | |||
+ | // Error: Named parameter $param overwrites previous argument | ||
+ | test(param: 1, param: 2); | ||
+ | // Error: Named parameter $param overwrites previous argument | ||
+ | test(1, param: 2); | ||
+ | </ | ||
+ | |||
+ | The first case is trivially illegal, because it specifies the same named argument twice. The second case is also illegal, because the positional argument and the named argument refer to the same parameter. | ||
+ | |||
+ | With the exception of variadic functions discussed below, specifying an unknown parameter name results in an '' | ||
+ | |||
+ | < | ||
+ | function test($param) { ... } | ||
+ | |||
+ | // Error: Unknown named parameter $parma | ||
+ | test(parma: "Oops, a typo" | ||
+ | </ | ||
+ | |||
+ | ==== Variadic functions and argument unpacking ==== | ||
+ | |||
+ | Functions declared as variadic using the '' | ||
<code php> | <code php> | ||
function test(...$args) { var_dump($args); | function test(...$args) { var_dump($args); | ||
- | test(1, 2, 3, a => ' | + | test(1, 2, 3, a: ' |
// [1, 2, 3, " | // [1, 2, 3, " | ||
</ | </ | ||
- | An example usage is the '' | + | The '' |
- | This feature is known as '' | + | <code php> |
+ | $params = ['start_index' | ||
+ | array_fill(...$params); | ||
+ | </ | ||
- | ==== Unpacking | + | Any value with a string key is unpacked as a named argument. Integers keys are treated as normal positional |
- | The '' | + | Argument unpacking is also subject to the general rule that positional arguments must always precede named arguments. Both of the following calls throw an '' |
+ | |||
+ | < | ||
+ | array_fill(...[' | ||
+ | array_fill(start_index: | ||
+ | </ | ||
+ | |||
+ | Furthermore, | ||
+ | |||
+ | < | ||
+ | test(...$values, | ||
+ | test(...$values, | ||
+ | </ | ||
+ | |||
+ | One of the primary use-cases for that variadic/ | ||
+ | |||
+ | < | ||
+ | function passthru(callable $c, ...$args) | ||
+ | return $c(...$args); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The support for named arguments in both variadics and argument unpacking ensures that this pattern will continue to work once named arguments are introduced. | ||
+ | |||
+ | ==== func_get_args() and friends ==== | ||
+ | |||
+ | The '' | ||
+ | |||
+ | < | ||
+ | function test($a = 0, $b = 1, $c = 2) { | ||
+ | var_dump(func_get_args()); | ||
+ | } | ||
+ | |||
+ | test(c: 5); | ||
+ | // Will behave exactly the same as: | ||
+ | test(0, 1, 5); | ||
+ | // Which is: | ||
+ | // array(3) { [0] => 0, [1] => 1, [2] => 5 } | ||
+ | </ | ||
+ | |||
+ | The behavior of '' | ||
+ | |||
+ | All three functions are oblivious to the collection of unknown named arguments by variadics. '' | ||
+ | |||
+ | ==== call_user_func() and friends ==== | ||
+ | |||
+ | Internal functions that perform some kind of "call forwarding", | ||
+ | |||
+ | < | ||
+ | |||
+ | $func = function($a = '', | ||
+ | echo "a: $a, b: $b, c: $c\n"; | ||
+ | } | ||
+ | |||
+ | // All of the following behave the same: | ||
+ | $func(' | ||
+ | call_user_func($func, | ||
+ | call_user_func_array($func, | ||
+ | </ | ||
+ | |||
+ | These calls are subject to the same restrictions as normal, for example there may not be positional arguments after named arguments. | ||
+ | |||
+ | For '' | ||
+ | |||
+ | While '' | ||
+ | |||
+ | ==== __call() ==== | ||
+ | |||
+ | Unlike '' | ||
+ | |||
+ | < | ||
+ | class Proxy { | ||
+ | public function __construct( | ||
+ | private object $object, | ||
+ | ) {} | ||
+ | public function __call(string $name, array $args) { | ||
+ | // $name == " | ||
+ | // $args == [1, " | ||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $proxy = new Proxy(new FooBar); | ||
+ | $proxy-> | ||
+ | </ | ||
+ | |||
+ | ==== Attributes ==== | ||
+ | |||
+ | Attributes also support named arguments: | ||
+ | |||
+ | < | ||
+ | << | ||
+ | class Test {} | ||
+ | </ | ||
+ | |||
+ | Similar to normal calls, trying to pass positional arguments after named arguments results in a compile-time error. Additionally, | ||
+ | |||
+ | The '' | ||
+ | |||
+ | < | ||
+ | var_dump($attr-> | ||
+ | // array(2) { | ||
+ | // | ||
+ | // | ||
+ | // | ||
+ | // | ||
+ | // } | ||
+ | </ | ||
+ | |||
+ | The '' | ||
+ | |||
+ | ==== Parameter name changes during inheritance ==== | ||
+ | |||
+ | Currently, parameter names are not part of the signature-contract. When only positional arguments are used, this is quite reasonable: The name of the parameter is irrelevant to the caller. Named arguments change this. If an inheriting class changes a parameter name, calls using named arguments might fail, thus violating the Liskov substitution principle (LSP): | ||
<code php> | <code php> | ||
- | $params = [' | + | interface I { |
- | strpos(...$params); // int(6) | + | public function test($foo, $bar); |
+ | } | ||
+ | |||
+ | class C implements I { | ||
+ | public function test($a, $b) {} | ||
+ | } | ||
+ | |||
+ | $obj = new C; | ||
+ | |||
+ | // Pass params according to I::test() contract | ||
+ | $obj-> | ||
</ | </ | ||
- | Any value with a string key is unpacked as a named parameter. Other key types (for arrays only integers) are treated as normal positional arguments. | + | [[https:// |
- | It's possible to unpack both positional | + | * Python |
+ | * C# and Swift introduce | ||
+ | * Kotlin warns on parameter name change | ||
- | ==== func_* and call_user_func_array ==== | + | Because we are retrofitting named arguments to an old language with a large body of existing code, we do not consider it sensible to unconditionally diagnose parameter name mismatches, especially considering that a lot of old code will never be invoked using named arguments. |
- | If (due to the usage of named arguments) some arguments | + | This RFC proposes |
- | * '' | + | This is a pragmatic approach that acknowledges that named arguments are not relevant for many methods, |
- | * '' | + | |
- | * '' | + | |
- | + | ||
- | All three functions | + | |
- | + | ||
- | The '' | + | |
- | Generally: The '' | + | As previously mentioned, this approach is also used by some existing languages, most notably Python, which is one of the languages with the heaviest usage of named arguments. This is hard evidence that such an approach does work reasonably well in practice, though of course the situations |
- | ===== Open questions ===== | + | The [[# |
- | ==== Syntax | + | ==== Internal functions |
- | The current | + | Historically, |
- | <code php> | + | Since PHP 8.0, it is possible to specify reflectible default values for internal functions, and this has already happened for functions which are bundled with the PHP distribution. This proposal is based on this default value information: |
- | test(foo => " | + | |
- | test(" | + | However, it is not possible to specify a sensible notion of " |
+ | |||
+ | <PHP> | ||
+ | function array_keys(array $arg, $search_value | ||
+ | </PHP> | ||
+ | |||
+ | The '' | ||
+ | |||
+ | Skipping such a parameter will result in an '' | ||
+ | |||
+ | < | ||
+ | // This is okay. | ||
+ | array_keys($array, | ||
+ | |||
+ | // Error: Argument #2 ($search_value) must be passed explicitly, | ||
+ | // because the default value is not known | ||
+ | array_keys($array, | ||
+ | </PHP> | ||
+ | |||
+ | I believe this is exactly the behavior we want, as specifying '' | ||
+ | |||
+ | The disadvantage of this general approach is that it requires default value information to be provided in order to work. 3rd-party extensions that do not provide this information (yet), will work with named arguments, but will not support skipping of arguments. | ||
+ | |||
+ | The alternative, | ||
+ | |||
+ | === Documentation / Implementation mismatches === | ||
+ | |||
+ | Currently, the parameter names used in the documentation and the implementation do not always match. If this proposal is accepted, we will synchronize the parameter names between both. This will also involve creating some naming guidelines, such as on the use of casing in parameter names. | ||
+ | |||
+ | === Internal APIs === | ||
+ | |||
+ | As outlined above, the existence of named arguments is mostly transparent for internal functions. Internal functions will see ordinary positional arguments, without any indication that the original call occurred via named arguments. As such, code adjustments will usually not be necessary. | ||
+ | |||
+ | One special case to consider are variadic functions, which will collect unknown named parameters into the '' | ||
+ | |||
+ | <PHP> | ||
+ | array_merge([1, | ||
+ | // ArgumentCountError: | ||
+ | </ | ||
+ | |||
+ | Functions that do want to accept extra unknown named arguments should use the '' | ||
+ | |||
+ | < | ||
+ | zval *args; | ||
+ | uint32_t num_args, | ||
+ | HashTable *extra_named; | ||
+ | ZEND_PARSE_PARAMETERS_START(0, | ||
+ | Z_PARAM_VARIADIC_WITH_NAMED(args, | ||
+ | ZEND_PARSE_PARAMETERS_END(); | ||
</ | </ | ||
- | The second syntax | + | The '' |
- | < | + | < |
- | test(array => [1, 2, 3]); // syntax error | + | typedef struct _zend_fcall_info { |
- | test(" | + | |
+ | | ||
+ | } zend_fcall_info; | ||
</ | </ | ||
- | The choice | + | Code that manually initializes '' |
- | <code php> | + | For convenience of implementation for '' |
- | // currently implemented: | + | |
- | test(foo => " | + | |
- | test(" | + | |
- | // suggestions (can use keywords): | + | ===== Backwards incompatible changes ===== |
- | test($foo | + | |
- | test(: | + | |
- | // suggestions (cannot use keywords): | + | In the narrow sense, this proposal has only one backwards-incompatible change: String keys in the '' |
- | test(foo = " | + | |
- | test(foo: " | + | |
- | // not possible because already valid code: | + | Next to this actual incompatibility, |
- | test($foo = " | + | |
- | </ | + | |
- | Which one(s) | + | First, as parameter names are now significant, |
- | ==== Collection of unknown named args into ...$opts ==== | + | Second, code may not be prepared to deal with unknown named arguments collected |
- | The current implementation / proposal suggests to use the '' | + | ===== Alternatives ===== |
- | Pro current solution: | + | ==== To named arguments ==== |
- | * Seems very PHP-like to do it this way, because PHP allows mixing " | + | There are two primary alternative implementation approaches for named arguments that I'm aware of, which will be briefly discussed in the following. |
- | + | ||
- | Con current solution: | + | |
- | * Having a separate syntax for capturing unknown named args makes the intention clearer: You don't always want to support **both** positional and named variadics. Separate syntax | + | First, |
- | + | ||
- | Opinions and arguments how to handle this are welcome. | + | |
- | ==== Unpacking | + | The big disadvantage of the opt-in approach is, of course, that named arguments would not work with any existing code (both userland and internal). I think that this would be a big loss to the feature, to the point that it might no longer be worthwhile. In particular, this would lose out on the object initialization use-case (as the syntax would not be usable in most cases), and would not help with old APIs, which tend to be particularly bad offenders when it comes to having many defaulted parameters and boolean flags. |
- | The same question comes up for argument unpacking: Should the '' | + | I think it would be more fruitful to provide an explicit opt-out mechanism, such as a '' |
- | In any case, this descision should mirror the one for the previous question. | + | Second, implementing named arguments as a side-effect of improved array destructuring functionality. As an example, let's return to the '' |
- | ==== Signature validation allows changing parameter names ==== | + | < |
+ | class ParamNode extends Node { | ||
+ | public string $name; | ||
+ | public ExprNode $default; | ||
+ | public TypeNode $type; | ||
+ | public bool $byRef; | ||
+ | public bool $variadic; | ||
- | Currently parameter names are not part of the signature-contract. When only positional arguments are used, this is quite reasonable: The name of the parameter is irrelevant to the caller. Named arguments change this. If an inheriting class changes a parameter | + | public function __construct(string $name, array $options) { |
+ | [ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ] = $options; | ||
- | <code php> | + | $this->name = $name; |
- | interface A { | + | |
- | public function test($foo, $bar); | + | |
+ | $this-> | ||
+ | $this-> | ||
+ | parent:: | ||
+ | } | ||
} | } | ||
+ | </ | ||
- | class B implements A { | + | This uses the existing syntax for array destructuring with keys, but additionally assumes support for destructuring default values, as well as destructuring type checks. As an additional step, we could support destructuring directly in the function signature: |
- | public function | + | |
+ | < | ||
+ | class ParamNode extends Node { | ||
+ | | ||
+ | public ExprNode $default; | ||
+ | public TypeNode $type; | ||
+ | public bool $byRef; | ||
+ | public bool $variadic; | ||
+ | |||
+ | | ||
+ | string | ||
+ | array [ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ], | ||
+ | | ||
+ | $this-> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | parent:: | ||
+ | | ||
} | } | ||
+ | </ | ||
- | $obj = new B; | + | While I think that improvements to array destructuring are worth pursuing, I don't think this covers the named parameter use-case satisfactorily. While this does take care of the type-safety concern, it still requires APIs to be specifically designed around an options array. |
- | // Pass params according to A::test() contract | + | Additionally, |
- | $obj-> | + | |
- | </ | + | ==== To parameter name changes during inheritance ==== |
+ | |||
+ | This RFC proposes to silently allow parameter name changes during inheritance. This is pragmatic, but may result in call-site errors when parameter names are changed and methods are invoked on child objects. An alternative is to automagically allow using parameter names from parent methods, as the following example illustrates: | ||
+ | |||
+ | < | ||
+ | interface I { | ||
+ | public function test($foo, $bar); | ||
+ | } | ||
+ | |||
+ | class C implements I { | ||
+ | public function test($a, $b) {} | ||
+ | } | ||
+ | |||
+ | $obj = new C; | ||
+ | |||
+ | // Pass params according to C::test() contract | ||
+ | $obj-> | ||
+ | // Pass params according to I::test() contract | ||
+ | $obj->test(foo: | ||
+ | </PHP> | ||
+ | |||
+ | Here using '' | ||
+ | |||
+ | Names from parent methods are registered as aliases, but not bound to a specific signature. As such, it's possible (though not recommended) to mix parameter names from different signatures: | ||
+ | |||
+ | < | ||
+ | // Use parameter names from both C::test() and I::test() | ||
+ | $obj-> | ||
+ | </ | ||
+ | |||
+ | From a design perspective it would be better to forbid such calls, but I don't believe that it is worth the technical and performance cost this would entail. | ||
+ | |||
+ | There is one problem with this scheme: What happens if two signatures share the same name at different positions? | ||
+ | |||
+ | < | ||
+ | interface I { | ||
+ | public function test($foo, $bar); | ||
+ | } | ||
+ | |||
+ | class C implements I { | ||
+ | public function test($bar, $foo) {} | ||
+ | } | ||
+ | |||
+ | // Fatal error: Parameter $foo of C::test() at position #2 conflicts with | ||
+ | // parameter $foo of I::test() at position #1 | ||
+ | </ | ||
+ | |||
+ | In this case, the LSP inheritance checks will report a fatal error. It is expected that this restriction will have much less impact in practice than a blanket prohibition of parameter renames, and that it will mostly point out legitimate LSP violations that hold even in the absence of named arguments. An analysis of affected cases in the top 2k composer packages can be found at https:// | ||
+ | |||
+ | Parameter names from prototype methods can come from a number of sources: | ||
+ | |||
+ | * Parent methods, including grand parents. | ||
+ | * Interface methods, including implementations of the same method from multiple interfaces. | ||
+ | * Abstract trait methods. | ||
+ | |||
+ | As such, a single parameter can have a potentially large number of aliases from a large number of prototypes. | ||
+ | |||
+ | A case that requires special consideration are parameters that are absorbed by a variadic in a child class: | ||
+ | |||
+ | < | ||
+ | class A { | ||
+ | public function method($a) {} | ||
+ | } | ||
+ | class B extends A { | ||
+ | public function method(...$args) {} | ||
+ | } | ||
+ | class C extends B { | ||
+ | public function method($c = null, ...$args) {} | ||
+ | } | ||
+ | |||
+ | (new B)-> | ||
+ | (new C)-> | ||
+ | </ | ||
+ | |||
+ | There are principally two ways in which this might behave: | ||
+ | |||
+ | < | ||
+ | // Option A: | ||
+ | (new B)-> | ||
+ | (new C)-> | ||
+ | |||
+ | // Option B: | ||
+ | (new B)-> | ||
+ | (new C)-> | ||
+ | </ | ||
+ | |||
+ | With option A, we would remember that '' | ||
+ | |||
+ | With option B, we instead discard parent parameters that are absorbed into a variadic. This means that the parameter '' | ||
+ | |||
+ | While I think this approach to the LSP problem is conceptually elegant, it turns out that it involves quite a few language design edge cases, as well as non-trivial technical complexity. | ||
+ | |||
+ | More importantly, | ||
+ | |||
+ | ===== Future Scope ===== | ||
+ | |||
+ | ==== Shorthand syntax for matching parameter and variable name ==== | ||
+ | |||
+ | Especially for constructors, | ||
+ | |||
+ | <PHP> | ||
+ | new ParamNode( | ||
+ | name: $name, | ||
+ | type: $type, | ||
+ | default: $default, | ||
+ | variadic: $variadic, | ||
+ | byRef: $byRef | ||
+ | ); | ||
+ | </ | ||
+ | |||
+ | Some languages offer special syntax (both for object initialization and destructuring) to avoid repeating the same name twice. Here is how such a syntax could look like in PHP, depending on the chosen named arguments syntax: | ||
+ | |||
+ | < | ||
+ | new ParamNode(: | ||
+ | new ParamNode(=$name, | ||
+ | new ParamNode(=> | ||
+ | </ | ||
+ | |||
+ | It should be noted that this problem is not specific to named arguments, and also affects array destructuring: | ||
+ | |||
+ | < | ||
+ | // What you have to write right now: | ||
+ | [' | ||
+ | </ | ||
+ | |||
+ | Analogously to the above examples, this could be written as: | ||
+ | |||
+ | < | ||
+ | [:$x, :$y, :$z] = $point; | ||
+ | [=$x, =$y, =$z] = $point; | ||
+ | [=> $x, => $y, => $z] = $point; | ||
+ | </ | ||
+ | |||
+ | Finally, this could also be useful for array construction, | ||
+ | |||
+ | < | ||
+ | return compact(' | ||
+ | |||
+ | // Could become: | ||
+ | return [:$x, :$y, :$z]; | ||
+ | return [=$x, =$y, =$z]; | ||
+ | return [=> $x, => $y, => $z]; | ||
+ | </ | ||
+ | |||
+ | If I wanted to put these ideas into a general framework, I think one way to go about this would be as follows: | ||
+ | |||
+ | * Consider '' | ||
+ | * Consider '': | ||
+ | |||
+ | Under this proposal, all three of the following would behave identically: | ||
+ | |||
+ | < | ||
+ | $point = [' | ||
+ | $point = [x: $x, y: $y, z: $z]; | ||
+ | $point = [:$x, :$y, :$z]; | ||
+ | </ | ||
+ | |||
+ | Approaching from this angle, the named argument syntax we should use is '' | ||
+ | |||
+ | ==== Positional-only and named-only parameters ==== | ||
+ | |||
+ | A useful extension of this proposal would be to allow parameters that can only be used positionally, | ||
- | If named parameters are introduced, signature validation should make sure that parameter names are not changed. Usually signature mismatches between an interface and an implementing class throw a fatal error, but this is not possible in this case due to the large BC break it would cause. Instead we could use some lower error type for this (warning / notice / strict). | + | ===== Vote ===== |
- | ===== Patch ===== | + | Voting opened 2020-07-10 and closes 2020-07-24. A 2/3 majority is required. |
- | You can find the diff for the work-in-programm patch here: https:// | + | <doodle title=" |
+ | * Yes | ||
+ | * No | ||
+ | </doodle> | ||
- | Credits: The patch includes some of the work that Stas' did for the skipparams RFC. | + | ===== Changelog ===== |
- | Work that still needs to be done: | + | * 2020-07-06: Move alternative LSP behavior |
+ | * 2020-07-06: Specify that call_user_func etc support named args. | ||
+ | * 2020-07-03: Add information on internal APIs. | ||
+ | * 2020-07-03: Explicitly mention behavior of attributes. | ||
+ | * 2020-06-23: Add alternative LSP behavior. | ||
+ | * 2020-06-23: Remove syntax as open question, specify use of '':'' | ||
+ | * 2020-05-05: RFC picked up again for PHP 8.0. | ||
+ | * 2013-09-09: '' | ||
- | * Implement the results of "Open questions" | ||
- | * Update all arginfos of internal functions to match the documentation (and improve names along the way). The current arginfo structs are hopelessly outdated. I hope that this work can be done mainly automatically. (Note: After named parameters are introduced the argument names are frozen and should not be changed.) | ||
- | * Make sure that internal functions properly handle skipped arguments. This should work in most cases automatically, |
rfc/named_params.1378505721.txt.gz · Last modified: 2017/09/22 13:28 (external edit)