rfc:simplified_named_params

PHP RFC: Simplified Named Arguments

This RFC is a simplified alternative to the Named Parameters RFC.

Introduction

Positional arguments passed to functions with a long argument list are unreadable, and often have to restate default values for optional arguments at the call site.

This RFC proposes the introduction of named arguments to be provided at call sites, using a simple syntax extension for function calls.

Named arguments simplify calls to functions with longer argument lists, particularly those with several optional arguments. They can also be used to increase readability at call sites by explicitly indicating parameter names.

Proposal

A regular function call to htmlspecialchars() might look as follows:

htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);

Providing the first argument as a positional argument, skipping the next two arguments, and providing the double_encode argument as a named argument, would look as follows:

htmlspecialchars($string, { double_encode: false });

In other words, the syntax extension for function and method calls consists an optional set of name/value pairs at the end of a call, after a comma, e.g. { name1: expr1, name2: expr2 } and so forth.

Each name must match the name of a parameter in the signature of the function or method being called - at the time of the call, if any name does not match a parameter name, an Error is thrown at the call site.

Each expression is any valid PHP expression.

Impact on Variadics

This proposal has no direct impact on variadics - a variadic argument may be provided by name, if required, just as any other named argument, but the provided value must evaluate to an array. If it does not, an error is generated, e.g. behavior consistent with passing a non-array variadic arguments dynamically via reflection.

Non-optional Defaults

PHP presently allows function declarations that specify default values for non-optional arguments - for example:

function hello($one = 1, $two) {
    // ...
}

This feature is currently meaningless, but is made meaningful by the introduction of named arguments - for example, the following call to the above function would be valid:

hello({ two: 2 });

Missing Arguments

The behavior for missing arguments in function calls is unaffected by this RFC - a missing argument will trigger a warning, just as it does today.

The behavior of func_num_args(), func_get_arg() and func_get_args() is unaffected - this RFC is strictly an extension to function call syntax.

Call Signature Inconsistency

Variation in argument names (between an abstract/interface/parent call signature and an implementation or override) can cause problems with name resolution.

For example:

interface CounterInterface {
    public function count($amount) {
        // ...
    }
}
 
class Counter implements CounterInterface {
    public function count($number) {
        // ...
    }
}
 
$c = new Counter();
 
$c->count({ amount: 123 }); // error

In this example, an interface specifies the first argument as amount, but the implementation specifies it as number - an attempt to provide amount as a named argument, in this case, will fail, because named arguments are matched against the names specified in the concrete implementation being invoked.

Since altering argument names in implementations is permitted in PHP, as per this proposal, the function call is considered invalid, and the code is considered valid.

This RFC does not propose the introduction of signature validation, which would be a considerable BC break.

Instead, upon encountering an unmatched named argument, a useful error message will be generated by scanning upwards through any parent class/interface declarations to find and report any possible inconsistencies - in the example above, the addition to the error message would be e.g. parameter names of method Counter::count() do not match those of CounterInterface::count().

Although this causes potential problems with named arguments applied to existing code, the problem is considered minor, since most existing code does not use long argument lists intended to be called using named arguments - most existing code with many options, for lack of better, likely uses a $options array argument, and uses code to explicitly unpack and apply the options. (This is discussed further in the section “Differences from Previous Proposal”, below.)

Magic Methods

Named arguments, are not applicable to magic methods implemented via call() or callStatic() - per this proposal, named arguments can only be matched to parameters that actually have names; parameters passed dynamically via these magic methods have only numeric indices.

As for implementations of __invoke(), these have actual argument lists, and therefore can be invoked using named arguments.

Impact on Reflection

This proposal has no impact on reflection.

Differences from Previous Proposal

Since this is a simplified proposal, compared to the one previously proposed by Nikita Popov, the following section will cover non-features and differences of this proposal as compared to that.

The syntax proposed by this RFC is intended to make named arguments more explicitly visible, by indicating with curly braces where named arguments begin and end. The syntax favors readability and explicitness over brevity. The other RFC uses the following invalid code example:

strpos(haystack => "foobar", "bar");

It's not immediately obvious to the eye where positional and named arguments being and end, nor is it easy to visually distinguish the array-like syntax from actual array expressions nearby. Delimiting named arguments with curly braces likely reduces the risk of creating an invalid function call while making changes at a call site.

The syntax deliberately does not borrow from array syntax, because named arguments, as proposed by this RFC, are not key/value pairs - the argument names are literal names, matched against literal parameter names, rather than name/value pairs being mapped to parameter lists.

To be clear, argument names cannot be specified using expressions or variables, cannot come from arrays, and cannot be resolved dynamically using code, at run-time, but must match literally - matching names to values at run-time is already possible using arrays, and this RFC takes the position that a named arguments feature does not need to overlap with that feature. To use a specific example from the other RFC:

$db->query(
    'SELECT * from users where firstName = :firstName AND lastName = :lastName AND age > :minAge',
    firstName => $firstName, lastName => $lastName, minAge => $minAge
);

This particular example is effectively no different from the following:

$db->query(
    'SELECT * from users where firstName = :firstName AND lastName = :lastName AND age > :minAge',
    ['firstName' => $firstName, 'lastName' => $lastName, 'minAge' => $minAge]
);

If the arguments are being resolved at run-time, the only difference is a subtle variation in syntax. This RFC deals strictly with named arguments, does not attempt to provide any additional functionality, and deliberately avoids feature overlap with existing features.

“Unknown named arguments” or “unpacking” of named arguments, per this RFC, is not a thing - function calls made using named arguments are identical to function calls made using positional arguments, the only difference being the ability to name which arguments are passed, and to specify those in any order, at the call site.

Due to concerns about backwards compatibility, this RFC does not propose the introduction of signature validation.

This RFC makes no attempt to support open sets (arrays) of options - the reason for this is best illustrated by the following non-valid example:

class Html {
    public static function tag($type, array $attrs) {
        // ...
    }
}
 
echo Html::tag('input', { name: $name, value: $value }); // NON-valid example

If this were permitted, except in terms of syntax, this would be no different from passing an array:

echo Html::tag('input', ['name' => $name, 'value' => $value ]);

In other words, open sets of options are already supported by arrays - argument lists are not open sets, and named arguments, per this proposal, are matched literally against the closed set of argument names defined by the function/method being called. Argument names are literal references to parameter declarations - they are not string values, and do not permit the use of undeclared dynamic “arguments”, since, effectively, such a feature would effectively be special syntax for passing a special array argument, rather than new syntax for passing named arguments.

One further reason for this point of view is illustrated by the following example:

function test($one = null, $two = null, $three = null) {
    // ...
}

Today, the following call is issued:

test({ one: 1, two: 2, three: 3});

Tomorrow, the function signature changes:

function test($one = null, $two = null) {
    // ...
}

If the argument list were an open set, the three argument would be quitly ignored, and the code would silently continue to run, but it wouldn't work.

Worse, imagine a case where an argument is renamed - the association at the call site is simply lost, with no indication of any problem; the code runs, but silently fails.

Dealing with these issues when using open sets of options, passed via arrays, is an issue you deliberately elect to deal with - the principle behind this RFC, is that named arguments must map strictly to declared parameters, otherwise you might as well be using a single array of options as the only argument.

Notes on Syntax

The proposed syntax resembles that of object literals in JS, and conflicts with an inactive RFC written in 2011. It does not appear to conflict with another declined RFC written in 2014, which proposed something similar for arrays rather than stdClass objects.

Regarding potential confusion over similarity to object literals in JS, curly braces are widely used in various languages to delimit the start and end of functions, conditional blocks, object literals, etc. - and arrays in PHP are probably closer in nature to JS objects than stdClass is, for example $o['k'] = 'v'; does not work on stdClass in PHP, but does work on arrays, which have a different syntax from JS object literals, as observed by the 2014 array literals proposal mentioned above, which proposes to build on PHP arrays, and array syntax, not on JS object literal syntax, although it makes clear the intended end is something similar to JS object literals.

If implemented, this RFC would stand in the way of adopting a JS-like syntax for object literals, but does not stand in the way of something else providing the same utility for arrays, which generally see more use and have greater utility than stdClass, due to the greater number of array functions, operators, and other features.

Backward Incompatible Changes

None

Proposed PHP Version(s)

Next PHP 7.x

RFC Impact

TBD

Proposed Voting Choices

TBD

Patches and Tests

There is no patch - this RFC needs a contributor.

rfc/simplified_named_params.txt · Last modified: 2021/01/13 15:22 by cmb