Hi Anton,

> On 23 May 2025, at 03:24, Anton Smirnov <sand...@sandfox.me> wrote:
> 
>> Moreover, what if I don't need a variadic parameter, but would like to 
>> declare that the function's interface requires named arguments (because I 
>> don't want to be tied to locking parameters to specific positions).
> 
> If you don't need a variadic, you write an ellipsis without a variable name, 
> like in my example:
> 
>>> function f(Type $pos1, Type $pos2, ..., Type $named1, Type $named2)

Ah, that hadn't really registered in my brain. So, if I want all parameters to 
be named, it'd be:

```php
function f(..., Type $named1, Typ $named2) { /* ... */ }
```

I still don't think I like this syntax. I don't think one should taint the 
declaration of the parameters with how the arguments are expected to be 
provided. Forcing named parameters is a way of declaring additional 
requirements for the interface of your function, and attributes are the go-to 
approach for this – think `#[\Override]`, `#[\Deprecated]`, etc.


>> So instead I'd propose to implement this using an attribute (e.g. 
>> `#[RequireNamedArgs]`) instead. That way, you could even make it so that 
>> only specific arguments are required to be named. It's also syntactically 
>> backwards compatible, so this feature wouldn't make your code break 
>> compatibility with older versions of PHP, and could be made available in 
>> older PHP versions using static analysis.
> 
> Please explain the syntax you propose about this part:
> 
>> That way, you could even make it so that only specific arguments are 
>> required to be named
> 
> because I see only 2 ways to call it:
> 
>  #[AllArgsAreNamed] function f (/* ... */)
> 
> and
> 
>  function f($arg1, #[AllArgsAfterThisOneAreNamed] $arg2, $arg3)
> 
> because
> 
>  function f($arg1, #[NamedArg] $arg2, /* positional again */ $arg3)
> 
> doesn't make sense, you cannot specify a positional argument after a 
> forced-named because it would be impossible to fill position 2 on the 
> function call.
> 
> Both cases are better covered by my syntax.

In this case it would imply all params including and after the `$arg2` have to 
be named as well. So in a way the attribute would replace your proposed `...,` 
notation 1:1.


>> It's also syntactically backwards compatible
> 
> Making it syntactically incompatible is what avoids the BC break

I think I understand what you mean, but the way you write that phrase makes it 
read like an oxymoron.

What I'm trying to get at here is libraries could start using the new attribute 
to tighten their internal library code without requiring consumers to bump 
their required PHP version straight away.


> relying on userland static analysis seems to be discouraged in this community.

I don't suggest we _rely_ on static analysis. However, consumers of a library 
that starts declaring their params as explicitly named could start getting 
hints in their editor or static analysis tool before bumping their PHP version.


> Consider this scenario:
> 
> let's say version x is the first one with the feature
> 
>  #[RequireNamedArgs]
>  function f($a, $b, $c) { /* ... */ }
> 
>  f(1, 2, 3); // works in x-1, broken in x
> 
> so this feature should at least go through the deprecation-like phase, and be 
> fully accepted only in a major version, which just means that it cannot be 
> fully used until the next major version. That defeats the purpose for me.

The same applies to `#[\Override]` and any other attributes that change PHP 
behavior. If someone decides to use them in an older version of PHP (through a 
polyfill or what not) it's their responsibility to ensure that the restrictions 
that would otherwise be imposed natively by PHP are sufficiently replaced 
through static analysis.
It is true that this expects some discipline from library developers to 
properly apply semantic versioning to their pacakges, but I much prefer this 
over being forced to bump my PHP requirements, both in my libraries and in my 
applications, before I can start using a feature.

Besides, even with the `...,` syntax, a certain amount of discipline is 
required from library developers at some point. Otherwise, even if I'm ont he 
most recent version of PHP, a package that starts enforcing named parameters 
without increasing their major version of their packages will just break my 
code once I update it.

In the end, the semantic versioning of PHP doesn't have to consider how a new 
PHP _feature_ is emulated on older versions of PHP through polyfills / static 
analysis.

Again my main issue with the `...,` syntax is that the goal of this feature is 
to hint that you intend to provide no guarantees about the positioning of the 
arguments. It shouldn't require backwards incompatible syntax, and there is 
precedent in how similar features have been implemented using attributes, 
namely: `#[\AllowDynamicProperties]`, `#[\Deprecated]`, `#[\Override]`, 
`#[\ReturnTypeWillChange]` and `#[\SensitiveParameter]`.

Additionally, I don't think a fatal Error should be thrown by this feature, at 
least not by default. An `E_USER_NOTICE` or `E_USER_DEPRECATED` would suffice 
to inform users that they have to take into account the guarantees around the 
signature of the function they're calling, but there is no reason to outright 
block otherwise perfectly fine function or method calls. I would like the 
ability to ease consumers of my library into using named arguments rather than 
forcing them to do so with a new major version release.
Having said that, using an attribute has the added advantage of being able to 
provide arguments to adjust its behavior. So for example, a `throw ` argument 
could be provided to hint that PHP should throw an error, if you really feel so 
strongly about the proper usage of our functions:

```php
#[\Named(throw: true)]
function f($a, $b, $c) { /* ... */ }

f(1, 2, 3); // Throws a `NamedArgumentException`.
```


> Anyway, the biggest problem for me, why I'm not considering writing an RFC 
> for now, is that I don't know what to do with func_get_args() and reflection.

Well, that's another benefit of using attributes; since they define the usage 
of the function at a different scope, `func_get_args()` and reflection will 
just continue to work the same as they already do. One could consider adding a 
`isNamed()` method to the `ReflectionParameter` class, but that's about it, I 
think.
Of course, if you foresee other issues with `func_get_args()` or reflection, 
I'd love to hear them.


Anyway, I hope I don't come off as too harsh. Fact is I really like the idea. I 
just think it makes more sense, and will have a better chance of succeeding, as 
an attribute.

Oh, come to think of it, it might also make sense to offer the opposite: a way 
to force positional arguments. What if I don't want users of my function to 
rely on a parameter name, but I will guarantee the consistent positions of the 
parameters... Again, this would be much more straightforward to implement as an 
attribute (e.g. `#[\Positional]`) while I'm not sure how that could be 
implemented using traditional syntax.

Alwin

Reply via email to