Hi internals,
I'd like to present an RFC for a first-class callable syntax, which is
intended as a simpler alternative to the partial function application (PFA)
proposal:
https://wiki.php.net/rfc/first_class_callable_syntax
See the Rationale section for details on how this relates to PFA. Over the
past week, we've had a lot of discussions on how exactly PFA is supposed to
work (most of them OTR), and while we seem to have a tentative consensus on
the correct model to use (which does not match the current RFC), it's clear
that this feature has turned out more complicated than originally
anticipated. Joe (who provided the implementation for the PFA RFC) is also
concerned about the implementation complexity that the final model would
require.
At least I personally was mainly interested in PFA because it provides a
first-class callable syntax as a side-effect. This RFC goes back to
providing just that. The syntax is forward-compatible with a future PFA
proposal though.
Regards,
Nikita
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.
Also, static analysers already have to reason about current code, so
PHPStan (and Psalm probably too) already supports referencing to callables
as strings (global functions) and arrays (methods):
$name = 'date';
$name(1); // Parameter #1 $format of callable 'date' expects string, int
given.
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.
Yes, I can see how that could be confusing. The current syntax is chosen to
be a subset of the partial function application proposal. However, I would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.
Also, static analysers already have to reason about current code, so
PHPStan (and Psalm probably too) already supports referencing to callables
as strings (global functions) and arrays (methods):$name = 'date';
$name(1); // Parameter #1 $format of callable 'date' expects string, int
given.
This is only possible for static analyzers that implement relatively
sophisticated and non-local analysis. If you see a random ['A', 'b'] in the
code, you cannot know whether this is just an array, or supposed to be a
reference to A::b() without following the data-flow of the value, and
checking whether it is going to be used in a position that requires a
callable, or passed to a callable parameter.
This means that answering a simple question like "find all references to
this method in the code base" also requires solving data-flow and type
inference problems, which is ... not great.
Apart from the static analysis perspective, having a first-class callable
syntax also fixes the current scoping problems that plague callables (which
can be worked around using Closure::fromCallable), and allows the
elimination of the callable type in favor of the Closure type, which has
much more well-defined semantics. (You will note that callable is already
not usable for typed properties.)
I don't think there's any question that we need a first-class callable
syntax ... it's more a question of how exactly it should look like. The PFA
RFC proposed one possibility. This RFC is another, more limited form.
Some people have also suggested that we should "just" allow writing strlen
to reference the strlen function etc, but this is not viable without a
prior unification of all symbol tables (such syntax conflicts with constant
access, class constant access and object property access).
Regards,
Nikita
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is chosen to
be a subset of the partial function application proposal. However, I would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.
Hi, several other syntaxes have been proposed to consideration in the PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?
Thanks,
--
Guilliam Xavier
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is chosen to
be a subset of the partial function application proposal. However, I would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.Hi, several other syntaxes have been proposed to consideration in the PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?Thanks,
I am much more into advanced features of PFA, but is this case why not
just use the & operator for "function reference" like C/C++? It is well
known and solves the ambiguity with method/function call.
$fn = &$this->myFunc;
$fn = &myFunc;
$fn = &Foo::myFunc;
On Thu, May 20, 2021 at 6:22 PM Luis Henrique luis@softsatsistemas.com.br
wrote:
On Thu, May 20, 2021 at 5:12 PM Nikita Popov nikita.ppv@gmail.com
wrote:Hi, I’m confused by the syntax, when I read it, I think to myself “I
know
this, this is just a method call… oh wait, it’s actually a callable”.
It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is
chosen to
be a subset of the partial function application proposal. However, I
would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.Hi, several other syntaxes have been proposed to consideration in the PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?Thanks,
I am much more into advanced features of PFA, but is this case why not
just use the & operator for "function reference" like C/C++? It is well
known and solves the ambiguity with method/function call.$fn = &$this->myFunc;
$fn = &myFunc;
$fn = &Foo::myFunc;
Unfortunately, this syntax is trivially ambiguous. "$fn = &$this->myFunc"
is currently already interpreted as a reference assignment of the property
$this->myFunc.
I have updated
https://wiki.php.net/rfc/first_class_callable_syntax#syntax_choice to
discuss some of the obvious suggestions and why they don't work.
Regards,
Nikita
$fn = &Foo::myFunc;
Unfortunately, this syntax is trivially ambiguous. "$fn = &$this->myFunc"
is currently already interpreted as a reference assignment of the property
$this->myFunc.
could that be just fn(sth)? I mean without the => part.
$fn = fn($this->myFunc);
$fn = fn(myFunc);
$fn = fn(Foo::myFunc);
and then something like this
$partial = fn(Foo::myFunc)[?, 42];
or
$partial = fn(Foo::myFunc, ?, 42);
My earlier idea was that actually cloning a callable could convert it to
a Closure, but I'm not sure now:
$fn = clone $this->myFunc;
or is this a heresy? ;)
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Ondřej Mirtes wrote:
Hi, I’m confused by the syntax, when I read it,...
That is true of almost any new syntax. Some of us are still getting
used to seeing \ in front of function calls. It doesn't mean that the
syntax choice is a bad one, just that it's something new.
For me, the main questions I ask myself is:
- could I explain what the syntax means to a junior developer?
- would the junior developer be able to remember it?
For both of those questions, the suggested syntax seems fine.
Guilliam Xavier wrote:
I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities
Unfortunately I don't think we do have a better place. Being able to
fork conversations so that "not completely off-topic, but not really
that productive and getting in the way of the main discussion stuff",
can be out of the way, but findable, is a feature that would be really
useful for discussions.
Nikita Popov wrote:
I am generally open to using a different syntax,
Well, you might regret saying that. It might not be appropriate*, but
having $() as marking a closure/callable seems 'clean' to me. Though
obviously novel and likely to cause gut reactions:
$(strlen);
$($foo, bar);
$(Foo, quux);
$($foo->fn);
$($foo);
$($fn);
Each would be equivalent to:
Closure::fromCallable('strlen');
Closure::fromCallable([$foo, 'bar']);
Closure::fromCallable([Foo::class, 'quux']); // aka
Closure::fromCallable('Foo::quux');
Closure::fromCallable($foo->fn);
Closure::fromCallable($foo);
Closure::fromCallable($fn);
For the code:
class Foo {
public $fn;
public function __construct($fn) {
$this->fn = $fn;
}
public function bar() {
}
public static function quux() {
}
public function __invoke() {
}
}
$fn = function() {
echo "I am function\n";
};
$foo = new Foo($fn);
cheers
Dan
Ack
- at least in the sense that it's a big syntax change, for a
relatively small feature.
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is chosen to
be a subset of the partial function application proposal. However, I would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.Hi, several other syntaxes have been proposed to consideration in the PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?Thanks,
There's been a lot of rapid iteration, experimentation, and rejection. The most recent alternatives are this one from Levi:
https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
-
Because of optional arguments, using the same symbol for "copy one parameter from the underlying function" and "copy all remaining parameters from the underlying function" is not viable. It runs into oddball cases where you may intend to only use one argument but end up using multiple, especially in array operation functions that sometimes silently pass keys along to callbacks if they can. Hence the separate ? and ... that were proposed.
-
Named arguments make things more complicated. One of the questions is whether named placeholders should be supported or not. And if they are, does that mean you can effectively reorder the arguments in the partial application, and what does that mean for usability. It gets complicated and scope-creepy fast.
-
The most important use cases are:
** "prefill nothing, just give me a callable" (which is the case Nikita's RFC covers)
** "reduce an arbitrary function to a single remaining argument", which lets it be used by various existing callback functions in the standard library (array_map, array_filter, etc.), many user-space APIs, and the pipes RFC that I am planning to bring back up once PFA is figured out (https://wiki.php.net/rfc/pipe-operator-v2).
While there are other use cases, the two of those cover the vast majority of uses. (There is some dispute about which of those is larger, or will be larger in the future.)
It took a while to realize the first point, which basically killed "? means zero or more". We also went down a rabbit hole of trying to make argument reordering work because some people asked for it, but as noted that's a very deep rabbit hole.
My gist above was a reduced-scope version of Levi's that uses two symbols and drops named placeholders. It doesn't give us every possible combination, but it does give us most reasonable combinations.
Nikita's "just do the first one" RFC (this thread) was proposed at about the same time, and takes an even-further scope reduction.
My own take is that the PFA discussion has been overly-bikeshedded, which is unfortunate since I think we're quite close to now having a workable answer that covers most reasonable use cases. While I agree Nikita's RFC here would be an improvement over 8.0, I don't think throwing in the towel on PFA yet is a good idea. It's a much more robust and powerful approach that still gets us the "first class callable" syntax we all want (at least I assume we all do), and lots of additional power to boot. I'd rather see us try to drive PFA home to completion. If that proves impossible by early July, then this RFC would still get us something this cycle, as long as the syntax is still compatible with PFA. (Otherwise whenever PFA gets sorted out in the future we end up with yet-more-ways to do the same thing that are not optimizations of each other but just competing syntax, in which case no one wins.)
--Larry Garfield
On Thu, May 20, 2021 at 8:38 PM Larry Garfield larry@garfieldtech.com
wrote:
There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
Intuitively, I think I prefer your algorithm as these are written up.
Levi's allows for named placeholders which I'm sure for some users is a big
draw. But I think what you're proposing is cleaner and will be better
understood. It resolves a lot of the concerns which came up in the PFA
thread, striking a reasonable balance between benefit and simplicity.
Either way suggests compatibility with Nikita's proposal, assuming use of
the spread operator as the implemented syntax. For this RFC in isolation,
though, it does loosely concern me as a user the proposed syntax looks more
like a function call. I won't bog anyone down arguing the syntax, it's a
minor detail for users to get used to in the grand scheme of things and I'd
definitely rather it was possible to build the more powerful PFA on top
than bin that off, or end up with a convoluted PHP providing competing ways
of doing the same thing.
- Named arguments make things more complicated. One of the questions is
whether named placeholders should be supported or not. And if they are,
does that mean you can effectively reorder the arguments in the partial
application, and what does that mean for usability. It gets complicated
and scope-creepy fast.
I do share the concern about named arguments and it's one of the reasons I
prefer your proposal. I think scaling back the scope a little bit is
exactly what's needed right now.
While I agree Nikita's RFC here would be an improvement over 8.0, I don't
think throwing in the towel on PFA yet is a good idea. It's a much more
robust and powerful approach that still gets us the "first class callable"
syntax we all want (at least I assume we all do), and lots of additional
power to boot. I'd rather see us try to drive PFA home to completion. If
that proves impossible by early July, then this RFC would still get us
something this cycle, as long as the syntax is still compatible with PFA.
I think this is very sensible, I can only really say I'd rather have
Nikita's proposal land in 8.1 and PFAs in 9.0 done right than have PFAs in
8.1 but in a way which is confusing, ambiguous or problematic for users, or
not covering reasonable expected use cases.
I think this is very sensible, I can only really say I'd rather have
Nikita's proposal land in 8.1 and PFAs in 9.0 done right than have PFAs in
8.1 but in a way which is confusing, ambiguous or problematic for users, or
not covering reasonable expected use cases.
I wholeheartedly agree with this sentiment. I hope it doesn't take until
9.0, but if we get first-class callables in 8.1 and partial application
in 8.2 those are BOTH huge new features.
And if by some miracle we come up with the perfect version of PFA a week
after this RFC passes, that's ALSO great.
Everyone: please let's keep this thread for talking about first-class
callables, and focus on the semantics not just the syntax - are there
edge cases we need to consider, downsides to the proposed
implementation, etc?
Regards,
--
Rowan Tommins
[IMSoP]
On Fri, May 21, 2021 at 10:08 AM Rowan Tommins rowan.collins@gmail.com
wrote:
Everyone: please let's keep this thread for talking about first-class
callables, and focus on the semantics not just the syntax - are there
edge cases we need to consider, downsides to the proposed
implementation, etc?
Well the semantics seem rather clear? We could say one "downside" is that
new-expressions are not supported, but the RFC explains why (
https://wiki.php.net/rfc/first_class_callable_syntax#object_creation, I
think it makes sense), and special-casing can always be added later (or in
PFA).
Regards,
--
Guilliam Xavier
Thank you all for your efforts, I think we're almost there and that PFA
would be a really great and useful addition to the language.
Le jeu. 20 mai 2021 à 21:38, Larry Garfield larry@garfieldtech.com a
écrit :
On Thu, May 20, 2021 at 5:12 PM Nikita Popov nikita.ppv@gmail.com
wrote:On Thu, May 20, 2021 at 4:16 PM Ondřej Mirtes ondrej@mirtes.cz
wrote:Hi, I’m confused by the syntax, when I read it, I think to myself “I
know
this, this is just a method call… oh wait, it’s actually a
callable”. It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is
chosen to
be a subset of the partial function application proposal. However, I
would
also be happy with some other syntax that makes it clearer that this is
acquiring a callable and not performing a call.Hi, several other syntaxes have been proposed to consideration in the PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?Thanks,
There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
- Because of optional arguments, using the same symbol for "copy one
parameter from the underlying function" and "copy all remaining parameters
from the underlying function" is not viable. It runs into oddball cases
where you may intend to only use one argument but end up using multiple,
especially in array operation functions that sometimes silently pass keys
along to callbacks if they can. Hence the separate ? and ... that were
proposed.
I've read that some ppl think this should be fought, but that's actually a
critical feature of the engine when writing BC layers by adding extra
arguments via this possibility.
In the 2nd gist, I read "If the current position is a ..., copy all
remaining parameters from the underlying function, including a variadic."
But to me it's important that all extra arguments are forwarded to the
partialized callable, where ... is used or not.
Please clarify this point if possible.
- Named arguments make things more complicated. One of the questions is
whether named placeholders should be supported or not. And if they are,
does that mean you can effectively reorder the arguments in the partial
application, and what does that mean for usability. It gets complicated
and scope-creepy fast.
For the userland pov, the principle of least surprise would say we should
answer yes to both questions.
We also went down a rabbit hole of trying to make argument reordering work
because some people asked for it, but as noted that's a very deep rabbit
hole.
Then maybe named args should be forbidden for "?" placeholders. This would
keep this possibility open of the future and close the "principle of least
surprise" expectation if there are. Aka foo(bar: ?) should be a parse error
for now at least.
My own take is that the PFA discussion has been overly-bikeshedded, which
is unfortunate since I think we're quite close to now having a workable
answer that covers most reasonable use cases. While I agree Nikita's RFC
here would be an improvement over 8.0, I don't think throwing in the towel
on PFA yet is a good idea. It's a much more robust and powerful approach
that still gets us the "first class callable" syntax we all want (at least
I assume we all do), and lots of additional power to boot. I'd rather see
us try to drive PFA home to completion. If that proves impossible by early
July, then this RFC would still get us something this cycle, as long as the
syntax is still compatible with PFA. (Otherwise whenever PFA gets sorted
out in the future we end up with yet-more-ways to do the same thing that
are not optimizations of each other but just competing syntax, in which
case no one wins.)
I agree. Let's figure out PFAs!
If I may add to the bikeshedding, and since PFAs are like others said
"short lambas on steroids", what about borrowing the syntax from them?
Here would be the first-class callable syntax:
fn(...) => $callable(...)
and for PFAs:
fn(...) => $callable(?, 42, ...)
Nicolas
Sorry for self-reply, this needs some clarifications :)
Le ven. 21 mai 2021 à 09:17, Nicolas Grekas nicolas.grekas+php@gmail.com
a écrit :
Thank you all for your efforts, I think we're almost there and that PFA
would be a really great and useful addition to the language.Le jeu. 20 mai 2021 à 21:38, Larry Garfield larry@garfieldtech.com a
écrit :On Thu, May 20, 2021 at 5:12 PM Nikita Popov nikita.ppv@gmail.com
wrote:On Thu, May 20, 2021 at 4:16 PM Ondřej Mirtes ondrej@mirtes.cz
wrote:Hi, I’m confused by the syntax, when I read it, I think to myself
“I know
this, this is just a method call… oh wait, it’s actually a
callable”. It
really makes my head hurt.Yes, I can see how that could be confusing. The current syntax is
chosen to
be a subset of the partial function application proposal. However, I
would
also be happy with some other syntax that makes it clearer that this
is
acquiring a callable and not performing a call.Hi, several other syntaxes have been proposed to consideration in the
PFA
thread, and I wouldn't want to start new bikeshedding here; is there a
place that would be more appropriate to gather the possibilities (like a
kind of updatable list)?Thanks,
There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
- Because of optional arguments, using the same symbol for "copy one
parameter from the underlying function" and "copy all remaining parameters
from the underlying function" is not viable. It runs into oddball cases
where you may intend to only use one argument but end up using multiple,
especially in array operation functions that sometimes silently pass keys
along to callbacks if they can. Hence the separate ? and ... that were
proposed.I've read that some ppl think this should be fought, but that's actually a
critical feature of the engine when writing BC layers by adding extra
arguments via this possibility.
I'm talking about implicit extra arguments accessed via func_get_args()
here.
In the 2nd gist, I read "If the current position is a ..., copy all
remaining parameters from the underlying function, including a variadic."
But to me it's important that all extra arguments are forwarded to the
partialized callable, where ... is used or not.
Please clarify this point if possible.
- Named arguments make things more complicated. One of the questions is
whether named placeholders should be supported or not. And if they are,
does that mean you can effectively reorder the arguments in the partial
application, and what does that mean for usability. It gets complicated
and scope-creepy fast.For the userland pov, the principle of least surprise would say we should
answer yes to both questions.
Reading your gists, I'm with drawing this expectation.
Since calls reorder named arguments automatically, we could expect the same
for PFAs.
That is:
since foo(A: 1, B: 2) is exactly the same as foo(B: 2, A: 1)
we could also expect that foo(A: ?, B: ?) would be exactly the same
foo(B:?, A:?)
The issue here is that the syntax can be interpreted as both a new
signature and as a partial call.
If we stay pure to the intend of PFAs, argument reordering shouldn't be
allowed IMHO. Aka this should be interpreted only as a partial call.
If we need a way to reorder arguments, I think it would be natural also
to be able to rename arguments.
That's where my syntax proposal based on short closures might be handy:
fn($a, $b, ...) => $callable($b, a: $a, ...)
We also went down a rabbit hole of trying to make argument reordering
work because some people asked for it, but as noted that's a very deep
rabbit hole.Then maybe named args should be forbidden for "?" placeholders. This would
keep this possibility open of the future and close the "principle of least
surprise" expectation if there are. Aka foo(bar: ?) should be a parse error
for now at least.My own take is that the PFA discussion has been overly-bikeshedded, which
is unfortunate since I think we're quite close to now having a workable
answer that covers most reasonable use cases. While I agree Nikita's RFC
here would be an improvement over 8.0, I don't think throwing in the towel
on PFA yet is a good idea. It's a much more robust and powerful approach
that still gets us the "first class callable" syntax we all want (at least
I assume we all do), and lots of additional power to boot. I'd rather see
us try to drive PFA home to completion. If that proves impossible by early
July, then this RFC would still get us something this cycle, as long as the
syntax is still compatible with PFA. (Otherwise whenever PFA gets sorted
out in the future we end up with yet-more-ways to do the same thing that
are not optimizations of each other but just competing syntax, in which
case no one wins.)I agree. Let's figure out PFAs!
If I may add to the bikeshedding, and since PFAs are like others said
"short lambas on steroids", what about borrowing the syntax from them?Here would be the first-class callable syntax:
fn(...) => $callable(...)
and for PFAs:
fn(...) => $callable(?, 42, ...)
Nicolas
Sorry for self-reply, this needs some clarifications :)
Le ven. 21 mai 2021 à 09:17, Nicolas Grekas nicolas.grekas+php@gmail.com
a écrit :
There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
- Because of optional arguments, using the same symbol for "copy one
parameter from the underlying function" and "copy all remaining parameters
from the underlying function" is not viable. It runs into oddball cases
where you may intend to only use one argument but end up using multiple,
especially in array operation functions that sometimes silently pass keys
along to callbacks if they can. Hence the separate ? and ... that were
proposed.I've read that some ppl think this should be fought, but that's actually a
critical feature of the engine when writing BC layers by adding extra
arguments via this possibility.I'm talking about implicit extra arguments accessed via
func_get_args()
here.
The issue is with optional arguments that would get auto-forwarded, even when you don't want to use them. (It wasn't obvious to me for a long time that this issue existed either.) The classic example, ported from Javascript:
function printInt(string $value, int $base = 10): void
{
echo intval($value, $base), "\n";
}
$array = ['10', '10', '10', '10', '10'];
array_walk($array, 'printInt');
Results in:
10
0
2
3
4
The function has multiple arguments, but we only want to use one, but functions like array_walk()
will try to use the optional one if they can... even if doing so makes no logical sense. Having ? capture all arguments unconditionally would also mean it captures optional arguments even if you don't want it to. That's why we need to have the separate ... symbol for when you really do mean "and all the rest".
In this case, we need printInt(?) to only pass $value through. If you really did want to use the array key for the base for some reason, you could do printInt(?, ?) or printInt(...).
I think the plan would be that extra passed arguments to a partialed function would still forward through, but that may end up being more difficult than expected.
In the 2nd gist, I read "If the current position is a ..., copy all
remaining parameters from the underlying function, including a variadic."
But to me it's important that all extra arguments are forwarded to the
partialized callable, where ... is used or not.
Please clarify this point if possible.
I think this is the same as the previous question?
(Note that several vocal people have argued that the way PHP passes along extraneous arguments is a bug, not a feature. I don't have a strong feeling about it, but some do.)
- Named arguments make things more complicated. One of the questions is
whether named placeholders should be supported or not. And if they are,
does that mean you can effectively reorder the arguments in the partial
application, and what does that mean for usability. It gets complicated
and scope-creepy fast.For the userland pov, the principle of least surprise would say we should
answer yes to both questions.Reading your gists, I'm with drawing this expectation.
Since calls reorder named arguments automatically, we could expect the same
for PFAs.
That is:
since foo(A: 1, B: 2) is exactly the same as foo(B: 2, A: 1)
we could also expect that foo(A: ?, B: ?) would be exactly the same
foo(B:?, A:?)The issue here is that the syntax can be interpreted as both a new
signature and as a partial call.
If we stay pure to the intend of PFAs, argument reordering shouldn't be
allowed IMHO. Aka this should be interpreted only as a partial call.If we need a way to reorder arguments, I think it would be natural also
to be able to rename arguments.
Which is also where the rabbit hole gets extra deep, and why I am perfectly happy to just disallow named placeholders. In the rare use cases where you really do want/need to reorder arguments and do other esoteric things, short lambdas already let you do that much more explicitly. That's an edge case that already has a solution. We're focused here on making the most common cases nicer.
That's where my syntax proposal based on short closures might be handy:
fn($a, $b, ...) => $callable($b, a: $a, ...)
At that point we're not getting any benefit over just using arrow functions. The point of PFA is that it's partially calling a function as-is, in a compact way. That in PHP we'd be implementing it as yet-another-closure object is an implementation detail.
We also went down a rabbit hole of trying to make argument reordering
work because some people asked for it, but as noted that's a very deep
rabbit hole.Then maybe named args should be forbidden for "?" placeholders. This would
keep this possibility open of the future and close the "principle of least
surprise" expectation if there are. Aka foo(bar: ?) should be a parse error
for now at least.
That is precisely the conclusion I reached and what my writeup says: The only placeholders are positional placeholders. Named arguments are fine, but not named placeholders.
My own take is that the PFA discussion has been overly-bikeshedded, which
is unfortunate since I think we're quite close to now having a workable
answer that covers most reasonable use cases. While I agree Nikita's RFC
here would be an improvement over 8.0, I don't think throwing in the towel
on PFA yet is a good idea. It's a much more robust and powerful approach
that still gets us the "first class callable" syntax we all want (at least
I assume we all do), and lots of additional power to boot. I'd rather see
us try to drive PFA home to completion. If that proves impossible by early
July, then this RFC would still get us something this cycle, as long as the
syntax is still compatible with PFA. (Otherwise whenever PFA gets sorted
out in the future we end up with yet-more-ways to do the same thing that
are not optimizations of each other but just competing syntax, in which
case no one wins.)I agree. Let's figure out PFAs!
If I may add to the bikeshedding, and since PFAs are like others said
"short lambas on steroids", what about borrowing the syntax from them?Here would be the first-class callable syntax:
fn(...) => $callable(...)
and for PFAs:
fn(...) => $callable(?, 42, ...)
Nicolas
As above, not really viable.
Also, to answer Andreas as long as I'm here: Supporting "new Foo" with PFA is a separate question. As noted in the currently written draft of the RFC, the problem is that object creation is not a function call; the object is created, then the __construct initializer is called. A naive PFA implementation would therefore create the object once, couple it to the partial, and then calling the partial repeatedly would return the same object, rather than repeatedly making a new object. Joe already figured out how to special case that, though, so it's a non-issue.
Both Levi's writeup and mine could support PFA constructors, AFAIK.
--Larry Garfield
Le ven. 21 mai 2021 à 16:31, Larry Garfield larry@garfieldtech.com a
écrit :
Sorry for self-reply, this needs some clarifications :)
Le ven. 21 mai 2021 à 09:17, Nicolas Grekas <
nicolas.grekas+php@gmail.com>
a écrit :There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfd
And this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
- Because of optional arguments, using the same symbol for "copy one
parameter from the underlying function" and "copy all remaining
parameters
from the underlying function" is not viable. It runs into oddball
cases
where you may intend to only use one argument but end up using
multiple,
especially in array operation functions that sometimes silently pass
keys
along to callbacks if they can. Hence the separate ? and ... that
were
proposed.I've read that some ppl think this should be fought, but that's
actually a
critical feature of the engine when writing BC layers by adding extra
arguments via this possibility.I'm talking about implicit extra arguments accessed via
func_get_args()
here.The issue is with optional arguments that would get auto-forwarded, even
when you don't want to use them. (It wasn't obvious to me for a long time
that this issue existed either.) The classic example, ported from
Javascript:function printInt(string $value, int $base = 10): void
{
echo intval($value, $base), "\n";
}$array = ['10', '10', '10', '10', '10'];
array_walk($array, 'printInt');
Results in:
10
0
2
3
4The function has multiple arguments, but we only want to use one, but
functions likearray_walk()
will try to use the optional one if they
can... even if doing so makes no logical sense. Having ? capture all
arguments unconditionally would also mean it captures optional arguments
even if you don't want it to. That's why we need to have the separate ...
symbol for when you really do mean "and all the rest".
Got it thanks. Makes sense to me now.
In this case, we need printInt(?) to only pass $value through. If you
really did want to use the array key for the base for some reason, you
could do printInt(?, ?) or printInt(...).I think the plan would be that extra passed arguments to a partialed
function would still forward through, but that may end up being more
difficult than expected.In the 2nd gist, I read "If the current position is a ..., copy all
remaining parameters from the underlying function, including a
variadic."
But to me it's important that all extra arguments are forwarded to the
partialized callable, where ... is used or not.
Please clarify this point if possible.I think this is the same as the previous question?
(Note that several vocal people have argued that the way PHP passes along
extraneous arguments is a bug, not a feature. I don't have a strong
feeling about it, but some do.)
Being vocal doesn't mean being right :)
This feature of the language is critical to write BC layers, see eg this,
LSP rulez:
https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php#L38
It has to stay (unless an alternative is provided of course.)
- Named arguments make things more complicated. One of the questions is
whether named placeholders should be supported or not. And if they
are,
does that mean you can effectively reorder the arguments in the
partial
application, and what does that mean for usability. It gets
complicated
and scope-creepy fast.For the userland pov, the principle of least surprise would say we
should
answer yes to both questions.Reading your gists, I'm with drawing this expectation.
Since calls reorder named arguments automatically, we could expect the
same
for PFAs.
That is:
since foo(A: 1, B: 2) is exactly the same as foo(B: 2, A: 1)
we could also expect that foo(A: ?, B: ?) would be exactly the same
foo(B:?, A:?)The issue here is that the syntax can be interpreted as both a new
signature and as a partial call.
If we stay pure to the intend of PFAs, argument reordering shouldn't be
allowed IMHO. Aka this should be interpreted only as a partial call.If we need a way to reorder arguments, I think it would be natural also
to be able to rename arguments.Which is also where the rabbit hole gets extra deep, and why I am
perfectly happy to just disallow named placeholders. In the rare use cases
where you really do want/need to reorder arguments and do other esoteric
things, short lambdas already let you do that much more explicitly. That's
an edge case that already has a solution. We're focused here on making the
most common cases nicer.That's where my syntax proposal based on short closures might be handy:
fn($a, $b, ...) => $callable($b, a: $a, ...)
At that point we're not getting any benefit over just using arrow
functions. The point of PFA is that it's partially calling a function
as-is, in a compact way. That in PHP we'd be implementing it as
yet-another-closure object is an implementation detail.
And the point of PFA is also to copy the existing signature, which is a
significant difference with short closures.
This completely disqualifies my proposal :)
But this also highlights a critical reason for adding PFAs: they're not
only syntax sugar to write short closure, but an abstraction over the
signature of partialized callables!
Last proposal, just to be sure we screened them all: using "...?" instead
of "..."
$callable(...?)
We also went down a rabbit hole of trying to make argument reordering
work because some people asked for it, but as noted that's a very deep
rabbit hole.Then maybe named args should be forbidden for "?" placeholders. This
would
keep this possibility open of the future and close the "principle of
least
surprise" expectation if there are. Aka foo(bar: ?) should be a parse
error
for now at least.That is precisely the conclusion I reached and what my writeup says: The
only placeholders are positional placeholders. Named arguments are fine,
but not named placeholders.My own take is that the PFA discussion has been overly-bikeshedded,
whichis unfortunate since I think we're quite close to now having a
workable
answer that covers most reasonable use cases. While I agree Nikita's
RFC
here would be an improvement over 8.0, I don't think throwing in the
towel
on PFA yet is a good idea. It's a much more robust and powerful
approach
that still gets us the "first class callable" syntax we all want (at
least
I assume we all do), and lots of additional power to boot. I'd
rather see
us try to drive PFA home to completion. If that proves impossible by
early
July, then this RFC would still get us something this cycle, as long
as the
syntax is still compatible with PFA. (Otherwise whenever PFA gets
sorted
out in the future we end up with yet-more-ways to do the same thing
that
are not optimizations of each other but just competing syntax, in
which
case no one wins.)I agree. Let's figure out PFAs!
If I may add to the bikeshedding, and since PFAs are like others said
"short lambas on steroids", what about borrowing the syntax from them?Here would be the first-class callable syntax:
fn(...) => $callable(...)
and for PFAs:
fn(...) => $callable(?, 42, ...)
Nicolas
As above, not really viable.
Also, to answer Andreas as long as I'm here: Supporting "new Foo" with PFA
is a separate question. As noted in the currently written draft of the
RFC, the problem is that object creation is not a function call; the object
is created, then the __construct initializer is called. A naive PFA
implementation would therefore create the object once, couple it to the
partial, and then calling the partial repeatedly would return the same
object, rather than repeatedly making a new object. Joe already figured
out how to special case that, though, so it's a non-issue.Both Levi's writeup and mine could support PFA constructors, AFAIK.
--Larry Garfield
Nicolas
There's been a lot of rapid iteration, experimentation, and rejection.
The most recent alternatives are this one from Levi:
https://gist.github.com/morrisonlevi/f7cf949c02f5b9653048e9c52dd3cbfdAnd this one from me:
https://gist.github.com/Crell/ead27e7319e41ba98b166aba89fcd7e8
The main takeaways (to give context to Nikita's proposal):
Because of optional arguments, using the same symbol for "copy one parameter from the underlying function" and "copy all remaining parameters from the underlying function" is not viable. It runs into oddball cases where you may intend to only use one argument but end up using multiple, especially in array operation functions that sometimes silently pass keys along to callbacks if they can. Hence the separate ? and ... that were proposed.
Named arguments make things more complicated. One of the questions is whether named placeholders should be supported or not. And if they are, does that mean you can effectively reorder the arguments in the partial application, and what does that mean for usability. It gets complicated and scope-creepy fast.
The most important use cases are:
** "prefill nothing, just give me a callable" (which is the case Nikita's RFC covers)
** "reduce an arbitrary function to a single remaining argument", which lets it be used by various existing callback functions in the standard library (array_map, array_filter, etc.), many user-space APIs, and the pipes RFC that I am planning to bring back up once PFA is figured out (https://wiki.php.net/rfc/pipe-operator-v2).While there are other use cases, the two of those cover the vast majority of uses. (There is some dispute about which of those is larger, or will be larger in the future.)
It took a while to realize the first point, which basically killed "? means zero or more". We also went down a rabbit hole of trying to make argument reordering work because some people asked for it, but as noted that's a very deep rabbit hole.
My gist above was a reduced-scope version of Levi's that uses two symbols and drops named placeholders. It doesn't give us every possible combination, but it does give us most reasonable combinations.
Nikita's "just do the first one" RFC (this thread) was proposed at about the same time, and takes an even-further scope reduction.
My own take is that the PFA discussion has been overly-bikeshedded, which is unfortunate since I think we're quite close to now having a workable answer that covers most reasonable use cases. While I agree Nikita's RFC here would be an improvement over 8.0, I don't think throwing in the towel on PFA yet is a good idea. It's a much more robust and powerful approach that still gets us the "first class callable" syntax we all want (at least I assume we all do), and lots of additional power to boot. I'd rather see us try to drive PFA home to completion. If that proves impossible by early July, then this RFC would still get us something this cycle, as long as the syntax is still compatible with PFA. (Otherwise whenever PFA gets sorted out in the future we end up with yet-more-ways to do the same thing that are not optimizations of each other but just competing syntax, in which case no one wins.)
I think Levis proposal and the two symbols ... and ? are a really good
improvement to the previous PFA RFC (Nikitas/Joes RFC is a good first
step in that direction). I do think named arguments are important, but
reordering arguments could be confusing and are not necessary (and if
named arguments are used for the PFA, it would make sense to use named
arguments for calling it too).
Another big reason for me why I like Levis proposal more is that he
still includes partially applying the new operator. Although the
semantics of the new operator are a bit different from regular
functions, it would be very handy to support this special case.
array_map is an obvious application:
$productObjects = array_map(new Product(?, ?, ?), $productIds,
$currencies, $prices);
But other usages would probably pop up, like abstracting away
dependencies of classes by pre-setting those via PFA, like:
$productFactory = new Product($dependency1, $dependency2, ...);
$productObjects = array_map($productFactory, $productIds, $currencies,
$prices);
I really like this because from a users perspective this seems very
clear and readable to me, even though from the PHP language perspective
it might be more messy.
About the bikeshedding: Using "..." as a symbol does make sense to me,
as variadic unpacking/variadic arguments have a similar connotation
(referring to an unknown/arbitrary amount of elements). * was also
suggested (as in "somefunction(*)" instead of "somefunction(...)"), and
I like that too - * in regex is any amount of elements (even zero), and
? is exactly one placeholder in SQL. As many PHP developers know regex
and SQL this would make PFA code a lot easier to understand even if
someone is not that familiar with it yet.
On Fri, May 21, 2021 at 11:09 AM Andreas Leathley a.leathley@gmx.net
wrote:
[...]
About the bikeshedding: Using "..." as a symbol does make sense to me,
as variadic unpacking/variadic arguments have a similar connotation
(referring to an unknown/arbitrary amount of elements). * was also
suggested (as in "somefunction(*)" instead of "somefunction(...)"), and
I like that too - * in regex is any amount of elements (even zero), and
? is exactly one placeholder in SQL. As many PHP developers know regex
and SQL this would make PFA code a lot easier to understand even if
someone is not that familiar with it yet.
FWIW: In this context, ? and * remind me of shell wildcards (used by glob()
and fnmatch()
), which makes sense too.
Also, at least one person is opposed to "...".
--
Guilliam Xavier
Hi, I’m confused by the syntax, when I read it, I think to myself “I know
this, this is just a method call… oh wait, it’s actually a callable”. It
really makes my head hurt.
I agree with the first point — slightly confusing initially, but I can see
myself get used to it. I suggested strlen(=>) as an alternative, not sure
if that would cause parsing difficulties.
Also, static analysers already have to reason about current code, so
PHPStan (and Psalm probably too) already supports referencing to callables
as strings (global functions) and arrays (methods):$name = 'date';
$name(1); // Parameter #1 $format of callable 'date' expects string, int
given.
I disagree here: while Psalm can understand what's going on in the above
code, callables are definitely (in my opinion) the most hairy part of PHP's
type system.
The RFC gives an example of something not caught by either PHPStan or Psalm:
https://psalm.dev/r/21217fdfb4
https://phpstan.org/r/6cc8cdc2-ecc6-403d-887d-0f12f4813b75
Discouraging this particular formulation would make affected codebases safe
from this particular error.
Best wishes,
Matt
Hi internals,
I'd like to present an RFC for a first-class callable syntax, which is
intended as a simpler alternative to the partial function application (PFA)
proposal:
This looks good to me. Following some discussions, I understand how this
syntax was proposed. Maybe update also the PFA RFC to be up to date with
the latest info.
Also, considering the resolution between property and method (or between
constants and static methods) , it's clear that we need a syntax that looks
like the usual invocation:(...), or (?), (...?), ($), ($$), (...$) etc.
I have few question related to some behavior.
If we have closure:
$c = function() {};
Will this be true?
$c(...) === $c;
similarly with how it happens for
Closure::fromCallable($c) === $c;
since the RFC makes a lot of comparisons between them.
Also, from performance point of view, do we expect the new syntax to behave
similarly?
And another not practical one:
Will this be valid?
$c(...)(...)(...)(...)();
Regards,
Alex
Also, considering the resolution between property and method (or between
constants and static methods) , it's clear that we need a syntax that looks
like the usual invocation:(...), or (?), (...?), ($), ($$), (...$) etc.
I'd like to expand on this point, because I'm seeing a lot of
suggestions for syntaxes that overlook it.
The following are all valid, but refer to different things called "foo":
foo // a constant called foo
foo() // a function called foo
$foo // a variable called foo
$bar->foo // a property called foo
$bar->foo() // a method called foo
Bar::foo // a class constant
Bar::foo() // a static method
Bar::$foo // a static property
The thing that consistently distinguishes function and method calls is
the parentheses after them. This holds even for "exotic" combinations:
$foo() // a variable called foo, de-referenced as a callable and then
invoked
($bar->foo)() // a property called foo, de-referenced as a callable and
then invoked
foo()() // a function called foo, the return value of which is
de-referenced as a callable and invoked
Any callable syntax (and, if/when we get one, a partial application
syntax) that doesn't have the parentheses there is going to run into
annoying edge cases fast. Not necessarily the kind of edge cases that
are hard for the parser, but definitely the kind that are confusing to
humans.
Regards,
--
Rowan Tommins
[IMSoP]
Hi Nikita,
I would like to just express my feelings. It's a definite YES from me for
first-class callables. We need something to replace [$this,
'privateMethod'].
I just don't like the proposed syntax. The triple period '...' has a
meaning already and reusing the same syntax isn't nice.
I haven't followed the PFA RFC closely, but I don't like that proposal as
it seems to add additional complexity to PHP's syntax for little gain. I
feel like it's just arrow function on steroids.
I know we can already do something like this:
function do_stuff($e) { return $e**2; }
$result = array_map(do_stuff::class, [1,2,3,4]);
I wouldn't mind having a dedicated operator for creating closures in the
same way. e.g.
$result = array_map(do_stuff::closure, [1,2,3,4]);
I understand that ::class creates a string literal and ::closure creates
closure, but from the usability point of view, they would be used in
similar ways.
Many languages have shared symbol tables for constants, functions and
variables. PHP doesn't, so we can't just use the name, but I agree with
Rowan that just using a function's name would be ambiguous even if the
parser allowed for that. We could introduce new syntax for this. I think
Kotlin uses double semicolon in front of the identifier. e.g.
$result = array_map(::do_stuff, [1,2,3,4]);
This would be less confusing than the (...) syntax IMHO. Of course this
still has the same ambiguity as Rowan points out. Is ::$objA->methA a
property or a method? We could solve this problem by specifying the syntax
to always refer to methods/functions. Parentheses could be used to enforce
access to property, e.g. ::($objA->methA) would evaluate the value stored
in the public property of the object $objA
If we land on using the syntax as your RFC proposes then I would ask
Internals to avoid (...) and instead use another symbol inside the
parentheses. I believe any other symbol would look better than triple
period, e.g. $objA->methA(*)
Kind Regards,
Kamil
This would be less confusing than the (...) syntax IMHO. Of course this
still has the same ambiguity as Rowan points out. Is ::$objA->methA a
property or a method? We could solve this problem by specifying the syntax
to always refer to methods/functions. Parentheses could be used to enforce
access to property, e.g. ::($objA->methA) would evaluate the value stored
in the public property of the object $objA
This is precisely the kind of "solution" that I referred to as easy for
the parser but confusing for humans.
::$objA->methA // method
(::$objA->methA) // method
::($objA->methA) // property
::($objA)->methA // method?
(::$objA)->methA // attempt to access a property of a Closure object?
It's just a mess, IMHO
Regards,
--
Rowan Tommins
[IMSoP]
Hi internals,
I'd like to present an RFC for a first-class callable syntax, which is
intended as a simpler alternative to the partial function application (PFA)
proposal:https://wiki.php.net/rfc/first_class_callable_syntax
See the Rationale section for details on how this relates to PFA. Over the
past week, we've had a lot of discussions on how exactly PFA is supposed to
work (most of them OTR), and while we seem to have a tentative consensus on
the correct model to use (which does not match the current RFC), it's clear
that this feature has turned out more complicated than originally
anticipated. Joe (who provided the implementation for the PFA RFC) is also
concerned about the implementation complexity that the final model would
require.At least I personally was mainly interested in PFA because it provides a
first-class callable syntax as a side-effect. This RFC goes back to
providing just that. The syntax is forward-compatible with a future PFA
proposal though.Regards,
Nikita
I've updated the RFC to forbid combination with the nullsafe operator, as
well as explicitly mention strict types behavior, as these came up in the
PFA discussion.
I plan to put up this RFC for voting in the event that the PFA proposal
does not get accepted.
Regards,
Nikita
Hi internals,
I'd like to present an RFC for a first-class callable syntax, which is
intended as a simpler alternative to the partial function application (PFA)
proposal:https://wiki.php.net/rfc/first_class_callable_syntax
See the Rationale section for details on how this relates to PFA. Over
the past week, we've had a lot of discussions on how exactly PFA is
supposed to work (most of them OTR), and while we seem to have a tentative
consensus on the correct model to use (which does not match the current
RFC), it's clear that this feature has turned out more complicated than
originally anticipated. Joe (who provided the implementation for the PFA
RFC) is also concerned about the implementation complexity that the final
model would require.At least I personally was mainly interested in PFA because it provides a
first-class callable syntax as a side-effect. This RFC goes back to
providing just that. The syntax is forward-compatible with a future PFA
proposal though.Regards,
NikitaI've updated the RFC to forbid combination with the nullsafe operator, as
well as explicitly mention strict types behavior, as these came up in the
PFA discussion.I plan to put up this RFC for voting in the event that the PFA proposal
does not get accepted.
As syntax has been the primary point of contention for this RFC, I've
considered adding a secondary vote for an alternative syntax, with the
prime contender being strlen::fn / $foo->bar::fn / Foo::bar::fn. I have
ultimately decided against this, because the ::fn syntax is not just a
minor syntax variation: It will result in materially different semantics.
While the end goal is the same, ::fn would approach it from a technically
quite different direction, by not piggy-backing on call syntax.
If this doesn't get accepted ("I like the idea but hate the syntax" is a
sensible reason to vote against), then I'd create a separate proposal for a
different syntax choice.
Regards,
Nikita
2021-05-20 14:48 GMT+02:00, Nikita Popov nikita.ppv@gmail.com:
Hi internals,
I'd like to present an RFC for a first-class callable syntax, which is
intended as a simpler alternative to the partial function application (PFA)
proposal:https://wiki.php.net/rfc/first_class_callable_syntax
See the Rationale section for details on how this relates to PFA. Over the
past week, we've had a lot of discussions on how exactly PFA is supposed to
work (most of them OTR), and while we seem to have a tentative consensus on
the correct model to use (which does not match the current RFC), it's clear
that this feature has turned out more complicated than originally
anticipated. Joe (who provided the implementation for the PFA RFC) is also
concerned about the implementation complexity that the final model would
require.At least I personally was mainly interested in PFA because it provides a
first-class callable syntax as a side-effect. This RFC goes back to
providing just that. The syntax is forward-compatible with a future PFA
proposal though.Regards,
Nikita
More bikeshedding, sorry. Was a syntax using fn
considered? Like
$fn = Closure::fromCallable('strlen');
$fn = fn strlen;
or
$fn = (fn) strlen;
Olle