Re: [RFC] Named arguments
| From: | John Bafford | Date: | Tue, 05 May 2020 17:27:54 +0000 |
| Subject: | Re: [RFC] Named arguments | ||
| References: | 1 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-110014@lists.php.net to get a copy of this message | ||
Hi Nikita,
> On May 5, 2020, at 09:51, Nikita Popov <nikita.ppv@gmail.com> wrote:
>
> Hi internals,
>
> I've recently started a thread on resurrecting the named arguments proposal
> (https://externals.io/message/109549), as this has come up tangentially in
> some recent discussions around attributes and around object ergonomics.
>
> I've now updated the old proposal on this topic, and moved it back under
> discussion: https://wiki.php.net/rfc/named_params
>
> Relative to the last time I've proposed this around PHP 5.6 times, I think
> we're technically in a much better spot now when it comes to the support
> for internal functions, thanks to the stubs work.
>
> I think the recent acceptance of the attributes proposal also makes this a
> good time to bring it up again, as phpdoc annotations have historically had
> support for named arguments, and this will make migration to the
> language-provided attributes smoother.
>
> Regards,
> Nikita
I very much do like the idea of named parameters, but I do not like the specific proposal in the
linked RFC at all. I think that treating named parameters simply as syntactic sugar is not a good
approach. If we're going to add named parameters, I think they should actually be significant
in their own right. As such, please allow me to present an alternative.
I would propose the following opt-in mechanism, which would work by treating the parameter names as
*part of the function name*. Doing it this way I _think_ should allow for no backwards compatibility
issues with existing code and would especially give library authors full control over the naming and
presentation of their APIs. Here is how it would work:
Let's start with an ordinary function:
function foo(int $a, int $b) : void {}
and then add named parameters:
function foo(a: int $a, b: int $b) : void {}
These are now two _completely different functions_. The first is named 'foo'. The second
is named 'foo(a:b:)'. They can both coexist in the same namespace. This would solve two
big classes of backwards compatibility issues.
First, adding the named-parameter function would not require any changes to any existing code. If
the named-parameter function were to become the preferred version, then whatever library implements
it could easily forward calls and add a deprecation warning to the old function:
function foo(int $a, int $b) : void { foo(a: $a, b: $b); }
Second, by having separate public and internal parameter names, it becomes possible to evolve APIs
without breaking existing users. If we start from our original two functions above and decide to fix
the poorly-named parameters, we could make these changes and continue to have backwards
compatibility with all existing users:
function foo(hours: int $hours, minutes: int $minutes) : void {}
function foo(a: int $hours, b: int $minutes) { foo(hours: $hours, minutes: $minutes }
function foo($hours, $minutes) { foo(hours: $hours, minutes: $minutes }
Changing the external parameter name counts as changing the function's name, which means
it's an entirely different function. (Which lets us also leave the old-named function there as
a fallback.) Changing the internal parameter name has no effect to the outside, and can be changed
at will. This also solves the problem with overloading: you can change the internal parameter names
all you like, so long as you keep the external parameter name - which is part of your declared API.
Because the parameter names are part of the function name, you would *not* be able to pick and
choose which parameter names you use. If the function declares parameter names, they must be used at
the call site.
Parameters are also positional. foo(a:..., b: ...) and foo(b: ..., a: ....) are two completely
different functions, by name. If you have foo(a:b:) defined, then you cannot call it as foo(b: ...,
a: ...).
Both of these last two provisions are to eliminate ambiguity and create consistency in the call
site. Otherwise we could easily wind up with confusion like in the implode and explode parameter
orderings.
You could allow having nameless parameters by doing this:
function bar(_: int $one, two: int $two) {}
bar(1, two: 2);
The "nameless" parameters would still be explicitly named ("_"), but users would
not have to provide that name at the call site. It would be permissible to mix named and unnamed
parameters, though style guides want to discourage that as there's probably few use-cases where
this makes sense:
function baz(holiday: string $name, _: string $date, color: string $color) {}
baz(holiday: 'Star Wars Day', 'May 4', color: 'purple');
There might be complexity around named and unnamed overlap with this special case:
function overlap($a, $b); //nominally named 'foo'
function overlap(_: $a, _: $b); //nominally named 'foo(_:_)'
In which case both functions would normally be called with the exact same syntax. We could declare
that, either the second form is explicitly disallowed; or it is allowed, but both may not exist in
the same scope. (I would prefer the latter, because it allows the function to be explicitly
referenced by symbol; see future-scope musings below.)
Named parameters will still all be positional, so you could still do something like:
call_user_func_array('baz:holiday:_:color', ['Star Wars Day', 'May
4', 'purple']);
Because named parameters are part of the function name, that would also allow us to have what would
appear to be name-based overloading, so we could have:
function __construct(name: string $name, ...) {}
function __construct(dto: SomeDTO $data) {}
$foo = new Holiday(name: 'Star Wars', ...):
$bar = new Holiday(dto: $formData);
This would be very useful for collections classes, where you could have:
$collection->insert($object):
$collection->insert(element: $object); //same as above, but more explicit
$collection->insert(contentsOf: $anotherCollection);
You could also use this to "solve" some of the more confusing php standard library
parameter ordering, entirely in userspace (or as part of the stdlib without BC breaks), by defining
new helper functions:
function strpos(haystack: string $haystack, needle: $needle, offset: int $offset = 0) : int {...}
function strpos(needle: $needle, haystack: string $haystack, offset: int $offset = 0) : int {...}
or even, reimagining the naming to be more like reading a sentence:
strpos($string, in: $someArray);
position(of: $string, in: $someArray)
str_repeat($string, count: 5);
implode($array, with: $glue);
explode($string, delimiter: $delimiter);
This means that rather than a one-size-fits-all approach for _every_ method in the standard library,
we could, on a case-by-case basis, add variants for where named parameters are explicitly helpful.
It would not be necessary to do this all-at-once; unaudited functions and methods would continue to
be used exactly as they always have been. New functions could offer either named-parameters and
no-named-parameters versions, whichever is most appropriate. (There could be both, but having two
names for the same thing is probably not good for API clarity unless one is intended as a BC shim.)
The exact details of the "mangled" name of functions is not important, so long as it is
unique for each function, and human-readable and typeable since the only way to reference a function
in a dynamic context is by a string name. For example, instead of 'foo(a:b:)',
'foo:a:b' could be used. I specifically picked 'foo(a:b:)' because it looks like
a function call (but without values, it's unambiguously not a call), and also opens the
possibility of using that specific syntax to allow for referring to functions by symbol, rather than
by string. That is, in a hypothetical future, you could do this:
var (callable<T>(T, T))[] $comparators = [
'string': \strcmp(_:_:)
\DateTime::class: self::specialDateComparator(_:_:),
A::class: A::compare(_:_:),
B::class: B:compare(first:second:),
C::class: function($a, $b) {...},
];
usort($array, $comparators[$eltType]);
But that's definitely getting into future-scope possibilities.
Thank you for your consideration. I can more fully flesh this out into an alternative RFC if people
would like.
-John