[RFC] Draft - Closure self reference

php.internals

Dan Ackroyd

5 years ago
Hello internals, For reasons, I was reviewing the conversation where adding closures to PHP was added, and it reminded me that currently the only way for a closure to call itself is slightly terribly, so I drafted an RFC: https://wiki.php.net/rfc/closure_self_reference Before I spend time on it, is there any strong reason why this is a bad idea? cheers Dan Ack

Unnamed Person

5 years ago
On Tue, Nov 10, 2020 at 10:09 AM Dan Ackroyd <Danack@basereality.com> wrote:
> > Hello internals, > > For reasons, I was reviewing the conversation where adding closures to > PHP was added, and it reminded me that currently the only way for a > closure to call itself is slightly terribly, so I drafted an RFC: > > https://wiki.php.net/rfc/closure_self_reference > > Before I spend time on it, is there any strong reason why this is a bad idea? > > cheers > Dan > Ack > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php
Is there any way we can we spell it `__FUNCTION__`?

Hans Henrik Bergan

5 years ago
something i'm missing from Javascript is the ability to give names to closures, this both gives closures the ability to reference themselves, but it also makes for meaningful stack traces, eg this is legal javascript: (function TheClosuresLocalName(){console.log(TheClosuresLocalName); throw new Error("look at my stacktrace!");})(); - the name is optional, and only visible inside the closure itself, and unfortunately this is not legal in PHP, i wish it was.

[ ]

5 years ago
Think the way it is implemented in JS is better than `$lambda` variable. At least it doesn't have BC breaks (does it?) On Tue, Nov 10, 2020, 7:38 PM Hans Henrik Bergan <divinity76@gmail.com> wrote:

Dan Ackroyd

5 years ago
On Tue, 10 Nov 2020 at 17:39, Hans Henrik Bergan <divinity76@gmail.com> wrote:
> > something i'm missing from Javascript is the ability to give names to > closures, ...the name is optional, and only visible inside the closure > itself, and unfortunately this is not legal in PHP, i wish it was.
I really like that...but unfortunately that wouldn't work in PHP. In JS, when a function is declared inside another function, the name of it is limited to the scope of the containing function. In PHP, when a function is declared inside another function, it is put into the current namespace's global scope. Changing how scope works in PHP would be too large a change for just this. Levi Morrison wrote:
> Is there any way we can spell it `__FUNCTION__`?
I think using some sort of constant, rather than a magic variable sounds is probably the way to go. I would hope we could find something better than that as: return __FUNCTION__($n-1) + __FUNCTION__($n-2); is pretty hard on my eyes. I think I'll wander over to the 'Support for <func>::function syntax' thread... cheers Dan Ack # PHP function foo1() { function bar() {} } function foo2() { function bar() {} } foo1(); foo2(); // Fatal error: Cannot redeclare bar() # JS function foo1() { var fn = function TheClosuresLocalName(){ console.log("I am closure inside foo1"); } return fn; } function foo2() { var fn = function TheClosuresLocalName(){ console.log("I am closure inside foo2"); } return fn; } fn1 = foo1(); fn2 = foo2(); fn1(); fn2(); "I am closure inside foo1" "I am closure inside foo2"

Christoph Becker

5 years ago
On 11.11.2020 at 18:39, Dan Ackroyd wrote:
> On Tue, 10 Nov 2020 at 17:39, Hans Henrik Bergan <divinity76@gmail.com> wrote: >> >> something i'm missing from Javascript is the ability to give names to >> closures, ...the name is optional, and only visible inside the closure >> itself, and unfortunately this is not legal in PHP, i wish it was. > > I really like that...but unfortunately that wouldn't work in PHP. > > In JS, when a function is declared inside another function, the name > of it is limited to the scope of the containing function. In PHP, when > a function is declared inside another function, it is put into the > current namespace's global scope. > > Changing how scope works in PHP would be too large a change for just this.
In JavaScript, a named function expression is different to a function declaration: var fn = function foo() {console.log('blah')} foo() => Uncaught ReferenceError: foo is not defined vs. function foo() {console.log('blah')} foo() => blah So the named function expression is still an anonymous function; the label is only defined inside of the function body. Christoph

David Rodrigues

5 years ago
I think that introducing a new variable, that even uncommon, could cause BC. My suggestion is to reuse the keyword `as` to set a variable that will represent its own closure. It is more flexible once that we could choose any name and reuse in nested closures. It is pretty similar to how SQL works too. function fn1() as $lambda1 { return function() as $lambda2 use ($lambda1) { return [ gettype($lambda1), gettype($lambda2) ]; }; } Atenciosamente, David Rodrigues Em qua., 11 de nov. de 2020 às 14:59, Christoph M. Becker <cmbecker69@gmx.de> escreveu:

Rowan Collins

5 years ago
On 11/11/2020 17:59, Christoph M. Becker wrote:
> In JavaScript, a named function expression is different to a function > declaration: > > var fn = function foo() {console.log('blah')} > foo() > => Uncaught ReferenceError: foo is not defined > > vs. > > function foo() {console.log('blah')} > foo() > => blah > > So the named function expression is still an anonymous function; the > label is only defined inside of the function body.
I think the key difference is that in JS, functions and variables occupy the same namespace, so "function foo" and "var foo = function" mean roughly the same thing. For the same reason, *inside* a named function expression, it acts as though there is a local variable with the given name: var fn = function foo() {console.log(typeof foo)}; fn(); // outputs 'function' which actually makes it very similar to... On 11/11/2020 18:37, David Rodrigues wrote:
> My suggestion is to reuse the keyword `as` to set a variable that will > represent its own closure. It is more flexible once that we could choose > any name and reuse in nested closures. It is pretty similar to how SQL > works too. > > function fn1() as $lambda1 { > return function() as $lambda2 use ($lambda1) { > return [ gettype($lambda1), gettype($lambda2) ]; > }; > }
I like this idea; it avoids reserving a magic variable name, but gives all the flexibility of having the full Closure object available (having a "constant" whose value was a Closure object would be somewhat odd). To take Dan's example from the RFC, this would no longer have any surprises: $fibonacci = function (int $n) as $fibonacci {     if ($n === 0) return 0;     if ($n === 1) return 1;     return $fibonacci($n-1) + $fibonacci($n-2); }; $this_is_the_original_fibonacci = $fibonacci; $fibonacci = function (int $n) { return rand(0, $n); }; echo $this_is_the_original_fibonacci(10). "\n"; The $fibonacci inside the closure would be a normal local variable, which just happens to have the same name as the one outside, so assigning to one would have no effect on the other. Regards,
-- Rowan Tommins (né Collins) [IMSoP]

Dan Ackroyd

5 years ago
On Wed, 11 Nov 2020 at 18:37, David Rodrigues <david.proweb@gmail.com> wrote:
> > My suggestion is to reuse the keyword `as` to set a variable that will represent its own > closure. It is more flexible once that we could choose any name and reuse in nested closures. > It is pretty similar to how SQL works too.
Hi David, That's a pretty reasonable suggestion, as it's less magic and less BC breaking. I've updated the words of the RFC to that, but am even less sure of how if/how that could be implemented... Michael Voříšek wrote:
> by allowing to specify/capture the variable after the function is assigned: > > $f = function () use ($f) {...};
Unfortunately that's not how PHP works. The assignment to $f doesn't happen until after the closure has been created. That's why currently the variable can be passed in by reference....but that has the undesirable problem that the RFC seeks to avoid. cheers Dan Ack

Sara Golemon

5 years ago
On Wed, Nov 11, 2020 at 12:37 PM David Rodrigues <david.proweb@gmail.com> wrote:
> My suggestion is to reuse the keyword `as` to set a variable that will > represent its own closure. It is more flexible once that we could choose > any name and reuse in nested closures. It is pretty similar to how SQL > works too. > > function fn1() as $lambda1 { > return function() as $lambda2 use ($lambda1) { > return [ gettype($lambda1), gettype($lambda2) ]; > }; > } > >
My initial reaction to this is: Technically doable (easy even), but looks a bit... ugly? weird? surprising? I think my main objection to it is how much is now stacked after the parameter list: function($args...) : returnType as $local use ($captures...) { ... }; That's a lot of... stuff (granted, we have most of it already) and how it looks when stacked together gets funky. Is it `returnType as $thing` ? That doesn't read well, but I don't see us making return type movable. as/use should be placable in either order, but maybe we give them required order anyway? TLDR; Long winded way to say I'm not inherently against this, and I like the idea of a lambda being able to recurse (not that I can ever recall having use for it, but maybe I fell back on named functions for that), I just think it's getting a bit wordy. -Sara

David Rodrigues

5 years ago
> Is it `returnType as $thing` ? That doesn't read well, but I don't see
us making return type movable. as/use should be placable in either order, but maybe we give them required order anyway? I don't know if that would be a problem, because today we can have it "function(): Type use($x)", so "Type use($x)"? I have to agree that it may sound strange at first, but I also can't imagine many ways of doing things differently without creating more complexities. (1) function as $lambda (... $args): returnType use ($captures...) {} (2) function (... $args) as $lambda: returnType use ($captures...) {} (3) function (... $args): returnType as $lambda use ($captures...) {} // original suggestion (4) function (... $args): returnType use ($captures...) as $lambda {} (4.b) function (... $args): returnType as $lambda {} // without use() will be same as (3) Or, define a "inline function" with its own name, visible only within the scope itself. Similar to how JS works. (5) function $lambda(... $args): returnType use ($captures...) {} To be honest, as I typed what came to mind, I ended up preferring this last option. JS way: function fb(n) { if (n === 0) return 0; if (n === 1) return 1; return fb(n - 1) + fb(n - 2); } PHP (self-referenced) way: function $fb($n): int { if ($n === 0) return 0; if ($n === 1) return 1; return $fb($n - 1) + $fb($n - 2); } Currently PHP will accepts the same JS solution, but it will exposes fb() to global scope and could causes errors: function test() { function fb($n): int { if ($n === 0) return 0; if ($n === 1) return 1; return fb($n - 1) + fb($n - 2); } var_dump(fb(10)); } test(); test(); // Fatal error: Cannot redeclare fb() Atenciosamente, David Rodrigues Em sex., 20 de nov. de 2020 às 12:14, Sara Golemon <pollita@php.net> escreveu:

Larry Garfield

5 years ago
On Fri, Nov 20, 2020, at 10:01 AM, David Rodrigues wrote:
> > Is it `returnType as $thing` ? That doesn't read well, but I don't see >> us making return type movable. as/use should be placable in either order, >> but maybe we give them required order anyway? > > I don't know if that would be a problem, because today we can have it > "function(): Type use($x)", so "Type use($x)"? I have to agree that it may > sound strange at first, but I also can't imagine many ways of doing things > differently without creating more complexities. > > (1) function as $lambda (... $args): returnType use ($captures...) {} > (2) function (... $args) as $lambda: returnType use ($captures...) {} > (3) function (... $args): returnType as $lambda use ($captures...) {} // > original suggestion > (4) function (... $args): returnType use ($captures...) as $lambda {} > (4.b) function (... $args): returnType as $lambda {} // without use() will > be same as (3) > > Or, define a "inline function" with its own name, visible only within the > scope itself. Similar to how JS works. > > (5) function $lambda(... $args): returnType use ($captures...) {} > > To be honest, as I typed what came to mind, I ended up preferring this last > option.
I kind of like that option, too. The main question in my mind is whether that means the same variable is used internally and externally or not. That is, my knee-jerk reaction when reading that was to ask if this would be confusing: $fib = function $f(int $n): int { ... } Viz, do $fib and $f need to match? It looks from the example below like you're saying it's actually: function $fib(int $n): int { ... } Where $fib is then both the internal and external variable name. That seems unexpected. It may be the best way to go about it, but it feels surprising as a user space developer. I do think something like that is probably the best way forward, though. The other question of course is short-lambdas. Would that mean this? fn $fib(int $n): int => match($n) { 0 => 0, 1 => 1, default => $fib(n - 1) + $fib($n - 2), }; If the function is inlined, does that mean the variable name leaks and thus remains in scope (and the closure thus using memory)? array_map(fn $fib(int $n): int => match($n) { 0 => 0, 1 => 1, default => $fib(n - 1) + $fib($n - 2), }, range(1, 6)); print $fib(5); // Does this work? (No you shouldn't write it like that, but since Fibonacci is the example we're working with...) --Larry Garfield

Sara Golemon

5 years ago
On Fri, Nov 20, 2020 at 11:40 AM Larry Garfield <larry@garfieldtech.com> wrote:
> > (5) function $lambda(... $args): returnType use ($captures...) {} > > > > To be honest, as I typed what came to mind, I ended up preferring this > last > > option. > > I kind of like that option, too.
Agreed that this has some attractiveness to it. What does returning by reference look like in that world though? $fn = function &$lambda($args...) {...}; Capturing lambda by reference doesn't make sense when you've just declared the var, but it looks confusing AF. $fn = function $lambda&($args...) {...}; Hello PERL...
> The main question in my mind is whether that means the same variable is > used internally and externally or not. >
The prototype I wrote in my brain for this would have $lambda exclusively within the closure's scope, not leaked to the declaring scope. I think I prefer that even though it looks a bit odd to have a name on a function that's not accessible from where it's defined. TL;DR - Still a little confusing. -Sara

Guilliam Xavier

5 years ago
On Fri, Nov 20, 2020 at 5:02 PM David Rodrigues <david.proweb@gmail.com> wrote:
> > Is it `returnType as $thing` ? That doesn't read well, but I don't see > us making return type movable. as/use should be placable in either order, > but maybe we give them required order anyway? > > I don't know if that would be a problem, because today we can have it > "function(): Type use($x)", so "Type use($x)"? >
No we can't, try it (Parse error). The correct order is "function() use($x): Type".
-- Guilliam Xavier

Sara Golemon

5 years ago
On Fri, Nov 20, 2020 at 12:58 PM Guilliam Xavier <guilliam.xavier@gmail.com> wrote:
> > I don't know if that would be a problem, because today we can have it > > "function(): Type use($x)", so "Type use($x)"? > > > > No we can't, try it (Parse error). The correct order is > "function() use($x): Type". > >
Ugh. That's ugly. I want to say we should fix it, but the horse has left the stables, so it's too late to enforce the right (to me) ordering. -Sara

Dan Ackroyd

5 years ago
Sara Golemon wrote:
> looks a bit... ugly? weird? surprising?
I had the same initial reaction, and was going to tell David what I thought of the suggestion. But after a couple of days, I was unable to actually articulate what I didn't like it, so think I was just having a gut reaction to something I hadn't seen before, and so wasn't used to seeing. Sara Golemon wrote:
> That's a lot of... stuff
Yeah, but it's a really rare use-case for a closure to need to be able to reference itself, so I think it's probably fine to have some verbosity around "this closure is doing something quite unusual". I personally think any of: function (... $args) as $lambda: returnType use ($captures...) {} function (... $args): returnType as $lambda use ($captures...) {} function (... $args): returnType use ($captures...) as $lambda {} would be fine and various 'my aesthetic choice is objectively the best' arguments, could be made for any of them. David Rodrigues wrote:
> I ended up preferring this last option.
I prefer the others as I think they are more deliberate syntax (i.e. don't look like a typo), avoid confusion around why can't normal functions be declared with leading $'s, and almost certainly would be easier to search for what they are. e.g. "php closure as variable" cheers Dan Ack

Michael Voříšek - ČVUT FEL

5 years ago
Hi everyone, maybe a bad idea, but what about addressing the "Principle of least astonishment" issue by allowing to specify/capture the variable after the function is assigned: $f = function () use ($f) {...}; With kind regards / Mit freundlichen Grüßen / S přátelským pozdravem, Michael Voříšek On 10 Nov 2020 18:08, Dan Ackroyd wrote:

Hans Henrik Bergan

5 years ago
if i have understood the "as"-suggestion correctly: $fn = function() as $lambdaOrAnyName {var_dump($lambdaOrAnyName);}; (function() as $lambdaOrAnyName{var_dump($lambdaOrAnyName);})(); then that syntax is fine with me. this is also very close to how it works in JavaScript (except the "as" part), i like the as-suggestion better than the $lambda suggestion in current RFC-draft, it has 0 bc break and people who want to call it $lambda can simply do function() as $lamda{};