Hi all,
I have been pondering for a while how to improve the anonymous class
syntax to allow "capturing" of values from the outer scope, and came up
with the idea of a special variable marker for "lexically captured
variable" - instead of $foo, you would write $!foo or $^foo (I quite
like the "upwardness" of $^).
To give a simple example, values can only pass into the anonymous class
via its constructor, like this:
function foo(int $outer) {
return new class($outer) {
public function __construct(
private int $myProp
) {}
public function getIt() {
return $this->myProp;
}
};
}
The idea is that you would instead be able to reference an outer
variable directly anywhere in the declaration, removing a lot of
boilerplate:
function foo(int $outer) {
return new class {
public function getIt() {
return $^outer;
}
};
}
The outer variable would be captured by value, and carried around with
the instance, like existing closures.
I suggest it is also treated as readonly, and visible strictly in the
lexical definition, not generally in private scope (it couldn't be
referenced from an applied trait, for instance).
Using it to initialise a property or local variable would allow you to
give it an explicit scope, while still avoiding the constructor:
function foo(int $outer) {
private $inner = $!outer;
return new class {
public function getIt() {
$this->inner++;
return $this->inner;
}
};
}
It then occurred to me that the same syntax could be used in
multi-statement anonymous functions instead of an explicit use()
statement. This strikes a different balance between conciseness and
explicitness than full automatic capture:
$before = function($x) use ($y, $z) {
$a = $x * $y;
return do_something($a, $z);
}
$after = function($x) {
$a = $x * $^y;
return do_something($a, $^z);
}
To keep this message short, I've put some more examples and thoughts
into a GitHub Gist here:
https://gist.github.com/IMSoP/4157af05c79b3df4c4853f5a58766341
I'd be interested to hear anyone's thoughts - is this a promising idea
to explore, or have I gone completely off the rails?
Regards,
--
Rowan Tommins
[IMSoP]
Hey Rowan
Hi all,
I have been pondering for a while how to improve the anonymous class
syntax to allow "capturing" of values from the outer scope, and came up
with the idea of a special variable marker for "lexically captured
variable" - instead of $foo, you would write $!foo or $^foo (I quite
like the "upwardness" of $^).To give a simple example, values can only pass into the anonymous class
via its constructor, like this:function foo(int $outer) {
return new class($outer) {
public function __construct(
private int $myProp
) {}
public function getIt() {
return $this->myProp;
}
};
}The idea is that you would instead be able to reference an outer
variable directly anywhere in the declaration, removing a lot of
boilerplate:function foo(int $outer) {
return new class {
public function getIt() {
return $^outer;
}
};
}
One thing to note is that, as I've learned recently, anonymous classes
can actually be instantiated at a later point with some tricks.
This could be avoided by adding the value to the constructor, but that
fully defeats the purpose of your proposal. Holding the value
indefinitely would create a leak and also doesn't really work as
createAnonymousClass
could be called multiple times and thus
capturing multiple $value
s. We could also decide to disallow
instantiations of anonymous classes in other places, that would
probably make most sense. This way the captured value could be
attached somewhere in the object and $^foo could access that instead.
That being said, I am indeed very skeptical if the added complexity is worth it.
// Lexical values could be used to initialse private, protected, or
public properties
// The same lexical value can be used any number of times
$x = 1;
$example = new class {
private $x = $^x;
protected $sharedX = $^x;
public $alsoX = $^x;
}
This could also prove technically challenging. Currently, property
defaults are constant ASTs and unique per class (not object). For your
case, they would need to be different per instance, which might
require quite a bit of refactoring.
As for the syntax in closures, that seems a bit more useful to me
personally, although I dislike the multi-nesting. Moving code, it
might be easy to miss that the number of "^" needs adjustments. I'd
personally prefer the previously proposed approach of capturing just
by name. Good naming should minimize the risk of clashing variables.
Regards,
Ilija
One thing to note is that, as I've learned recently, anonymous classes
can actually be instantiated at a later point with some tricks.
Huh, that's freaky... I guess this is all a reminder that these really
are anonymous classes, not anonymous instances - at some point, a
class entry needs to be generated.
I guess you'd have to generate a new class entry every time the "new
class" line was run, and inject the extra values into that.
If it was limited to capturing scalars and arrays, you could treat it as
a kind of macro expansion, i.e. this ...
$example = new class {
public $inner = $^outer;
}
... could be a sort of sugar for:
eval(
sprintf(
'return new class {
public $inner = %s;
};',
var_export($outer, true)
)
);
Which is valid code, if not particularly efficient: https://3v4l.org/sQaUS
That doesn't allow for normal object semantics, though, so is probably a
non-starter.
This could also prove technically challenging. Currently, property
defaults are constant ASTs and unique per class (not object).
As I understand it, the main reason object properties weren't included
in the "new in initializers" RFC was the problem of when side effects
would occur:
https://wiki.php.net/rfc/new_in_initializers#unsupported_positions
That's not an issue here, because we're not creating an object, we're
"just" resolving the $^outer token to an existing zval, and storing it
in the default property table.
It might actually be easier to only allow capture into property
initialisers:
- Create a new class entry without any property default (or clone a
pre-compiled base) - Add the captured values to the default property table
- Create an instance, run its constructor, etc, as currently
Or even:
- Use the existing logic to create an anonymous class entry ignoring
the capturing syntax - Create an instance, initialise normal defaults, but don't run the
constructor yet - Push the captured values directly into the instance properties
- Run the constructor
I'm probably in way over my head here, so I should probably stop here,
in the hope that it will inspire someone more knowledgeable to come up
with something workable, because I'd really love for anonymous classes
to be more flexible than they are.
Regards,
--
Rowan Tommins
[IMSoP]
I guess you'd have to generate a new class entry every time the "new
class" line was run, and inject the extra values into that.If it was limited to capturing scalars and arrays, you could treat it as
a kind of macro expansion, i.e. this ...$example = new class {
public $inner = $^outer;
}... could be a sort of sugar for:
eval(
sprintf(
'return new class {
public $inner = %s;
};',
var_export($outer, true)
)
);Which is valid code, if not particularly efficient: https://3v4l.org/sQaUS
Unfortunately, PHP isn't really well suited for something like that.
Eval'd classes are still request-persistent, so any created object
would leak its class structure (which is much bigger and less
optimized in terms of memory than the object).
Normal anonymous classes: https://3v4l.org/41UGH
Anonymous classes created through eval (and thus creating a separate
class): https://3v4l.org/Q9eE3
Ilija
Hi all,
I have been pondering for a while how to improve the anonymous class
syntax to allow "capturing" of values from the outer scope, and came up
with the idea of a special variable marker for "lexically captured
variable" - instead of $foo, you would write $!foo or $^foo (I quite
like the "upwardness" of $^).To give a simple example, values can only pass into the anonymous class
via its constructor, like this:function foo(int $outer) {
return new class($outer) {
public function __construct(
private int $myProp
) {}
public function getIt() {
return $this->myProp;
}
};
}The idea is that you would instead be able to reference an outer
variable directly anywhere in the declaration, removing a lot of
boilerplate:function foo(int $outer) {
return new class {
public function getIt() {
return $^outer;
}
};
}The outer variable would be captured by value, and carried around with
the instance, like existing closures.I suggest it is also treated as readonly, and visible strictly in the
lexical definition, not generally in private scope (it couldn't be
referenced from an applied trait, for instance).Using it to initialise a property or local variable would allow you to
give it an explicit scope, while still avoiding the constructor:function foo(int $outer) {
private $inner = $!outer;
return new class {
public function getIt() {
$this->inner++;
return $this->inner;
}
};
}It then occurred to me that the same syntax could be used in
multi-statement anonymous functions instead of an explicit use()
statement. This strikes a different balance between conciseness and
explicitness than full automatic capture:$before = function($x) use ($y, $z) {
$a = $x * $y;
return do_something($a, $z);
}
$after = function($x) {
$a = $x * $^y;
return do_something($a, $^z);
}To keep this message short, I've put some more examples and thoughts
into a GitHub Gist here:
https://gist.github.com/IMSoP/4157af05c79b3df4c4853f5a58766341I'd be interested to hear anyone's thoughts - is this a promising idea
to explore, or have I gone completely off the rails?
Pourque no los dos?
Ilija's point about implementation challenges concerns me, but my initial thought is that I quite like it. I've run into the same issue with capturing into anon classes and wished for something like this.
As you were describing it, I was also thinking of the potential implications for closures, too. That sounds... kinda fun. :-) However, I agree with Ilija that the original proposal to just do it automatically would be better; and if that didn't pass, I have no expectation that an alternate with a funky new syntax would do any better.
The other concern is that this introduces a whole new realm of possible behaviors; like, what does $^foo mean in a non-anon-class context? Does it? Could it be made to mean something in the future? Does it become an alternate syntax for global
? What else could we do with it? This rabbit hole goes very deep very quickly, and pursuing this for anon classes without considering the longer-term implications seems both impossible (because people will ask) and unwise.
--Larry Garfield
However, I agree with Ilija that the original proposal to just do it automatically would be better; and if that didn't pass, I have no expectation that an alternate with a funky new syntax would do any better.
Well, a large part of my resistance to automatic capture is that it
makes variable scope less visible at a glance. This avoids that by still
having a marker for "I am from another scope", but a much less verbose
one than the current use() clause.
I may be in a minority of one on that point, though, for all I know.
The other concern is that this introduces a whole new realm of possible behaviors...
That's kind of where the closure thought came from - it seemed a very
close analogue, so having one syntax for both feels natural. Outside of
those cases, though, there's no reason it should mean anything, just as
$this->foo or self::$foo doesn't mean anything outside a class. In fact,
it could be spelled capture::$foo or $scope->foo rather than just using
new punctuation, if we wanted to encourage that analogy.
Regards,
--
Rowan Tommins
[IMSoP]
Outside of
those cases, though, there's no reason it should mean anything, just as
$this->foo or self::$foo doesn't mean anything outside a class. In fact,
it could be spelled capture::$foo or $scope->foo rather than just using
new punctuation, if we wanted to encourage that analogy.
Would this be allowed in files included in the methods of the anonymous class?
$this->
and self::
is, and it's a pain point for static analyzers,
forcing us to invent
things like @psalm-scope-this
--
Best regards,
Bruce Weirdan mailto:weirdan@gmail.com
On Wed, Mar 15, 2023 at 1:09 AM Rowan Tommins rowan.collins@gmail.com
wrote:
Well, a large part of my resistance to automatic capture is that it
makes variable scope less visible at a glance. This avoids that by still
having a marker for "I am from another scope", but a much less verbose
one than the current use() clause.
How about first implementing use() for anonymous classes first?
Something like:
function foo(int $outer) {
return new class() use($outer) {
public function getIt() {
return $outer;
}
};
}
This would help with the main problem you expressed and be a less concern
for the future.
It's a different direction from what you suggested but it might be a lot
harder to pass a $^ syntax or similar.
Regards,
Alex
How about first implementing use() for anonymous classes first?
Something like:function foo(int $outer) {
return new class() use($outer) {
public function getIt() {
return $outer;
}
};
}
I think it's a lot less clear exactly where the variable is being imported to that way. Would you still be able to overwrite $outer as a normal local variable in that function scope? Would it return to its captured value every time the function runs? It's different from capturing into a function, because you're not just adding a variable to a single existing scope.
Just allowing the captured var for property initialisation would be less ambiguous, but having to list it in use() and then in the property list would be the same amount of code as you can currently get to with constructor promotion (see my first example in the thread).
From what Ilija has pointed out, the implementation of actually getting the values into the right place is going to be harder than handling the syntax anyway. My current feeling is that if it's possible at all, it will end up as a shorthand for that constructor example: declaring normal properties, and populating them as the instance is initialised.
Regards,
--
Rowan Tommins
[IMSoP]
How about first implementing use() for anonymous classes first?
Something like:function foo(int $outer) {
return new class() use($outer) {
public function getIt() {
return $outer;
}
};
}I think it's a lot less clear exactly where the variable is being imported to that way. Would you still be able to overwrite $outer as a normal local variable in that function scope? Would it return to its captured value every time the function runs? It's different from capturing into a function, because you're not just adding a variable to a single existing scope.
Just allowing the captured var for property initialisation would be less ambiguous, but having to list it in use() and then in the property list would be the same amount of code as you can currently get to with constructor promotion (see my first example in the thread).
From what Ilija has pointed out, the implementation of actually getting the values into the right place is going to be harder than handling the syntax anyway. My current feeling is that if it's possible at all, it will end up as a shorthand for that constructor example: declaring normal properties, and populating them as the instance is initialised.
Regards,
--
Rowan Tommins
[IMSoP]--
To unsubscribe, visit: https://www.php.net/unsub.php
The outer variable would be captured by value, and carried around with
the instance, like existing closures.
For what its worth, there are ways to get references inside values:
Other than pointing that out, I like it.
On Wed, Mar 15, 2023 at 1:09 AM Rowan Tommins rowan.collins@gmail.com
wrote:Well, a large part of my resistance to automatic capture is that it
makes variable scope less visible at a glance. This avoids that by still
having a marker for "I am from another scope", but a much less verbose
one than the current use() clause.How about first implementing use() for anonymous classes first?
Something like:function foo(int $outer) {
return new class() use($outer) {
public function getIt() {
return $outer;
}
};
}This would help with the main problem you expressed and be a less concern
for the future.It's a different direction from what you suggested but it might be a lot
harder to pass a $^ syntax or similar.Regards,
Alex
For the record, while the auto-capture RFC[1] didn't reach a 2/3 majority to pass, it did have a strong majority support (62%). Given that, I think it unlikely that a "require verbose manual capture in more places" proposal would fair that well.
[1] https://wiki.php.net/rfc/auto-capture-closure
--Larry Garfield
Hi Rowan,
I have been pondering for a while how to improve the anonymous class
syntax to allow "capturing" of values from the outer scope, and came up
with the idea of a special variable marker for "lexically captured
variable" - instead of $foo, you would write $!foo or $^foo (I quite
like the "upwardness" of $^).
To overcome the issues spotted in the thread, what about doing some sort of
CPP instead of autocapture?
new class (...$arguments) use ($outer) extends Foo {
public function getIt() {
return $this->outer;
}
}
This would be the equivalent of this:
new class ($outer, ...$arguments) extends Foo {
public function __construct(public mixed $outer, ...$arguments) {
parent::__construct(...$arguments);
}
public function getIt() {
return $this->outer;
}
}
And we could also allow this for better type expressivity:
new class (...$arguments) use (private int $outer) extends Foo {
// ...
}
Nicolas
On Thu, 16 Mar 2023 at 09:28, Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
To overcome the issues spotted in the thread, what about doing some sort
of CPP instead of autocapture?new class (...$arguments) use ($outer) extends Foo {
public function getIt() {
return $this->outer;
}
}This would be the equivalent of this:
new class ($outer, ...$arguments) extends Foo {
public function __construct(public mixed $outer, ...$arguments) {
parent::__construct(...$arguments);
}
public function getIt() {
return $this->outer;
}
}
I was actually just thinking about exactly that approach, and wondering it
would be possible to do it entirely as an AST rewrite.
My only uncertainty so far is what to do with an actual constructor in the
class, like this:
new class($custom) use ($captured) {
public function __construct($custom) {
// Duplicate definition error?
// Silently renamed and called from the generated constructor?
// Merged into the body after the generated lines?
}
}
Forbidding it wouldn't be the worst restriction, but if there was some
per-instance setup logic needed, not being able to write a constructor body
might be a pain.
And we could also allow this for better type expressivity:
new class (...$arguments) use (private int $outer) extends Foo {
// ...
}
I was going to suggest an "as" clause, similar to traits, which would also
allow naming the property differently from the source variable:
$foo = 42;
$name = 'Bob';
$class = new class use ($foo as private int $counter, $name as readonly
string) {}
Equivalent to:
$foo = 42;
$name = 'Bob';
$class = new class($foo, $name) {
public function __construct(private int $counter, public readonly
string $name) {}
}
Or in full:
$foo = 42;
$name = 'Bob';
$class = new class($foo, $name) {
private int $counter;
public readonly string $name;
public function __construct(int $counter, string $name) {
$this->counter = $counter;
$this->name = $name;
}
}
Regards,
Rowan Tommins
[IMSoP]
To overcome the issues spotted in the thread, what about doing some sort
of CPP instead of autocapture?new class (...$arguments) use ($outer) extends Foo {
public function getIt() {
return $this->outer;
}
}This would be the equivalent of this:
new class ($outer, ...$arguments) extends Foo {
public function __construct(public mixed $outer, ...$arguments) {
parent::__construct(...$arguments);
}
public function getIt() {
return $this->outer;
}
}I was actually just thinking about exactly that approach, and wondering it
would be possible to do it entirely as an AST rewrite.My only uncertainty so far is what to do with an actual constructor in the
class, like this:new class($custom) use ($captured) {
public function __construct($custom) {
// Duplicate definition error?
// Silently renamed and called from the generated constructor?
// Merged into the body after the generated lines?
}
}Forbidding it wouldn't be the worst restriction, but if there was some
per-instance setup logic needed, not being able to write a constructor body
might be a pain.
We could define the "use" as declaring + setting the properties before the
constructor is called, if any.
But I'm also fine making both constructs conflict: when there is a
constructor, the boilerplate saved by the "use" becomes really low.
No strong opinion either. There could be other factors to consider.
And we could also allow this for better type expressivity:
new class (...$arguments) use (private int $outer) extends Foo {
// ...
}I was going to suggest an "as" clause, similar to traits, which would also
allow naming the property differently from the source variable:$foo = 42;
$name = 'Bob';
$class = new class use ($foo as private int $counter, $name as readonly
string) {}
I like that!
We could define the "use" as declaring + setting the properties before
the constructor is called, if any.
But I'm also fine making both constructs conflict: when there is a
constructor, the boilerplate saved by the "use" becomes really low.
No strong opinion either. There could be other factors to consider.
The main advantage of generating an actual constructor is that no change
is needed to the shared object initialization code, which could be
complex and even have performance impact. The only new logic would be in
compiling the class entry and putting the arguments into the "new
class(...)" statement.
The more I think about it, the more I'm leaning to just blocking custom
constructors, and saying you can either have the use() short-hand, or
you can have custom initialization. Additional functionality can always
be added in later if someone comes up with a clean implementation and a
good use case.
Regards,
--
Rowan Tommins
[IMSoP]
We could define the "use" as declaring + setting the properties before
the constructor is called, if any.
But I'm also fine making both constructs conflict: when there is a
constructor, the boilerplate saved by the "use" becomes really low.
No strong opinion either. There could be other factors to consider.The main advantage of generating an actual constructor is that no change
is needed to the shared object initialization code, which could be
complex and even have performance impact. The only new logic would be in
compiling the class entry and putting the arguments into the "new
class(...)" statement.The more I think about it, the more I'm leaning to just blocking custom
constructors, and saying you can either have the use() short-hand, or
you can have custom initialization. Additional functionality can always
be added in later if someone comes up with a clean implementation and a
good use case.
Wouldn't the functionality described boil down to essentially just materializing into a few extra lines in the constructor? At least to my ignorant non-engine brain it seems straightforward to have this:
$a = 1;
$b = 2;
$c = 3;
$o = new class ($a, $b) use ($c) {
public function __construct(private int $a, private int $b) {}
public function something() {}
}
Desugar to this:
$c = class ($a, $b) use ($c) {
private $c;
public function __construct(private int $a, private int $b) {
$this->c = 3; // By value only, so this should be fine?
}
public function something() {}
}
--Larry Garfield
Wouldn't the functionality described boil down to essentially just materializing into a few extra lines in the constructor? At least to my ignorant non-engine brain it seems straightforward to have this:
$a = 1;
$b = 2;
$c = 3;$o = new class ($a, $b) use ($c) {
public function __construct(private int $a, private int $b) {}
public function something() {}
}Desugar to this:
$c = class ($a, $b) use ($c) {
private $c;
public function __construct(private int $a, private int $b) {
$this->c = 3; // By value only, so this should be fine?
}
public function something() {}
}
Not quite - as Ilija pointed out, the class definition gets compiled
once, but the capture needs to happen for every instance, with
(potentially) different values of $c. In other words, $c needs to be
injected as a constructor argument, not a constant in the class definition.
That's still fine, in principle - you can compile to this:
$o = class ($a, $b, $c) {
public function __construct(private int $a, private int $b, private $c) {
}
public function something() {}
}
Or once constructor promotion is de-sugared as well, this:
$o = class ($a, $b, $c) {
private int $a;
private int $b;
private $c;
public function __construct($a, $b, $c) {
$this->a = $a; // from constructor promotion
$this->b = $b; // from constructor promotion
$this->c = $c; // from use() statement
// other lines from body of constructor go here
}
public function something() {}
}
It just introduces a lot of extra cases to handle:
- If there's no constructor, create one
- If there is a constructor with other arguments, merge the argument
lists; since there will then be an explicit argument list to "new
class()", merge those lists as well - Maybe different handling if those other arguments are already using
constructor promotion, as in this example - If there are existing lines in the constructor body, combine those
with the auto-generated assignments
Which is why I'm thinking a first implementation would be reasonable
which just took this approach:
- "new class" can either have an argument list or a use() statement, not
both - the use() statement generates a constructor at the top of the class
- if the class body already contains a constructor, the compiler will
complain that you have two methods named "__construct"
Regards,
--
Rowan Tommins
[IMSoP]
Wouldn't the functionality described boil down to essentially just materializing into a few extra lines in the constructor? At least to my ignorant non-engine brain it seems straightforward to have this:
$a = 1;
$b = 2;
$c = 3;$o = new class ($a, $b) use ($c) {
public function __construct(private int $a, private int $b) {}
public function something() {}
}Desugar to this:
$c = class ($a, $b) use ($c) {
private $c;
public function __construct(private int $a, private int $b) {
$this->c = 3; // By value only, so this should be fine?
}
public function something() {}
}Not quite - as Ilija pointed out, the class definition gets compiled
once, but the capture needs to happen for every instance, with
(potentially) different values of $c. In other words, $c needs to be
injected as a constructor argument, not a constant in the class definition.That's still fine, in principle - you can compile to this:
$o = class ($a, $b, $c) {
public function __construct(private int $a, private int $b, private $c) {
}
public function something() {}
}Or once constructor promotion is de-sugared as well, this:
$o = class ($a, $b, $c) {
private int $a;
private int $b;
private $c;public function __construct($a, $b, $c) {
$this->a = $a; // from constructor promotion
$this->b = $b; // from constructor promotion
$this->c = $c; // from use() statement
// other lines from body of constructor go here
}
public function something() {}
}It just introduces a lot of extra cases to handle:
- If there's no constructor, create one
- If there is a constructor with other arguments, merge the argument
lists; since there will then be an explicit argument list to "new
class()", merge those lists as well- Maybe different handling if those other arguments are already using
constructor promotion, as in this example- If there are existing lines in the constructor body, combine those
with the auto-generated assignmentsWhich is why I'm thinking a first implementation would be reasonable
which just took this approach:
- "new class" can either have an argument list or a use() statement, not
both- the use() statement generates a constructor at the top of the class
- if the class body already contains a constructor, the compiler will
complain that you have two methods named "__construct"
Ah, fair enough. I'd want to see a better error message than that (which would be confusing for people who don't know what they're looking for), but otherwise that's a reasonable first iteration. Especially as I can't recall when I last had an anon class constructor that was doing anything other than manual closures. :-)
--Larry Garfield
Hi Rowan, hi all!
Le ven. 17 mars 2023 à 15:51, Larry Garfield larry@garfieldtech.com a
écrit :
Wouldn't the functionality described boil down to essentially just
materializing into a few extra lines in the constructor? At least to my
ignorant non-engine brain it seems straightforward to have this:$a = 1;
$b = 2;
$c = 3;$o = new class ($a, $b) use ($c) {
public function __construct(private int $a, private int $b) {}
public function something() {}
}Desugar to this:
$c = class ($a, $b) use ($c) {
private $c;
public function __construct(private int $a, private int $b) {
$this->c = 3; // By value only, so this should be fine?
}
public function something() {}
}Not quite - as Ilija pointed out, the class definition gets compiled
once, but the capture needs to happen for every instance, with
(potentially) different values of $c. In other words, $c needs to be
injected as a constructor argument, not a constant in the class
definition.That's still fine, in principle - you can compile to this:
$o = class ($a, $b, $c) {
public function __construct(private int $a, private int $b, private
$c) {
}
public function something() {}
}Or once constructor promotion is de-sugared as well, this:
$o = class ($a, $b, $c) {
private int $a;
private int $b;
private $c;public function __construct($a, $b, $c) {
$this->a = $a; // from constructor promotion
$this->b = $b; // from constructor promotion
$this->c = $c; // from use() statement
// other lines from body of constructor go here
}
public function something() {}
}It just introduces a lot of extra cases to handle:
- If there's no constructor, create one
- If there is a constructor with other arguments, merge the argument
lists; since there will then be an explicit argument list to "new
class()", merge those lists as well- Maybe different handling if those other arguments are already using
constructor promotion, as in this example- If there are existing lines in the constructor body, combine those
with the auto-generated assignmentsWhich is why I'm thinking a first implementation would be reasonable
which just took this approach:
- "new class" can either have an argument list or a use() statement, not
both- the use() statement generates a constructor at the top of the class
- if the class body already contains a constructor, the compiler will
complain that you have two methods named "__construct"Ah, fair enough. I'd want to see a better error message than that (which
would be confusing for people who don't know what they're looking for), but
otherwise that's a reasonable first iteration. Especially as I can't
recall when I last had an anon class constructor that was doing anything
other than manual closures. :-)
I created this draft RFC to help move things forward:
https://wiki.php.net/rfc/syntax-to-capture-variables-when-declaring-anonymous-classes
Please let me know your early thoughts and I'd be happy to move it to
"under discussion".
I'd also need someone for the implementation as I doubt I'll be able to
write it myself in a reasonable time!
Cheers,
Nicolas
On Thu, 13 Apr 2023 at 13:40, Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
I created this draft RFC to help move things forward:
https://wiki.php.net/rfc/syntax-to-capture-variables-when-declaring-anonymous-classes
Please let me know your early thoughts and I'd be happy to move it to
"under discussion".
I'd also need someone for the implementation as I doubt I'll be able to
write it myself in a reasonable time!
Hi Nicolas!
Thanks, it's really encouraging to have some interest in the feature.
The good news is that I have an implementation of this nearly ready (it
passes all my tests, but the code's a bit messy). I was hoping to polish it
over the Easter weekend and draft a PR, but wasn't feeling very well.
It has full support for visibility, types, references, and property
renaming, but no merging of parameter lists or automatic call to the parent
constructor. I think with clean errors that's a good first feature set, and
if someone comes up with an implementation for manipulating existing
constructors, that can be proposed later.
So, "watch this space", as the saying goes :)
Regards,
Rowan Tommins
[IMSoP]
Hi Nicolas,
czw., 13 kwi 2023 o 14:40 Nicolas Grekas nicolas.grekas+php@gmail.com
napisał(a):
Hi Rowan, hi all!
Le ven. 17 mars 2023 à 15:51, Larry Garfield larry@garfieldtech.com a
écrit :Wouldn't the functionality described boil down to essentially just
materializing into a few extra lines in the constructor? At least to my
ignorant non-engine brain it seems straightforward to have this:$a = 1;
$b = 2;
$c = 3;$o = new class ($a, $b) use ($c) {
public function __construct(private int $a, private int $b) {}
public function something() {}
}Desugar to this:
$c = class ($a, $b) use ($c) {
private $c;
public function __construct(private int $a, private int $b) {
$this->c = 3; // By value only, so this should be fine?
}
public function something() {}
}
Have you thought about not populating property by default but instead:
- adding "use" language construct as default to all methods?
- adding ability to assign variable values from "use" to property if needed?
Like desugar to
$c = class ($a, $b) use ($c) {
private $c = $c; // optional, not required, no conflicts
public function __construct(private int $a, private int $b) use ($c) {
$this->c = $c % 2 ? 3 : 5;
}
public function something() use ($c) {
return $c;
}
}
Or there is something so wrong with this thinking I cannot see yet.
Cheers,
Michał Marcin Brzuchalski
Have you thought about not populating property by default but instead:
- adding "use" language construct as default to all methods?
- adding ability to assign variable values from "use" to property if needed?
Like desugar to
$c = class ($a, $b) use ($c) {
private $c = $c; // optional, not required, no conflicts
public function __construct(private int $a, private int $b) use ($c) {
$this->c = $c % 2 ? 3 : 5;
}
public function something() use ($c) {
return $c;
}
}
This is closer to what I originally proposed, but with different syntax.
The challenge, as was pointed out to me, is that the anonymous class is compiled as a reusable class definition on first use, and a new instance construction on each execution, so you have to think about what happens at each stage.
In this example, you've got to first compile something where $c is an open parameter of some sort, and make it available to all the contexts where it might be used; then, when you create the instance, supply a value for $c, and attach it to the instance somewhere; and finally, when something() is called, look up that attached value and make it available to the method.
In other words, the use() clause on the class ends up acting like a constructor parameter, and the references in methods end up acting like property references. It's a lot of extra complication to duplicate things the language already has.
Having played with it a bit while implementing, I also like the conciseness of objects with no explicit body at all:
$who = new class use ($firstName as string, $lastName as string) {};
echo $who->firstName;
Regards,
--
Rowan Tommins
[IMSoP]
Em qui., 13 de abr. de 2023 às 09:40, Nicolas Grekas <
nicolas.grekas+php@gmail.com> escreveu:
I created this draft RFC to help move things forward:
https://wiki.php.net/rfc/syntax-to-capture-variables-when-declaring-anonymous-classes
Please let me know your early thoughts and I'd be happy to move it to
"under discussion".
I'd also need someone for the implementation as I doubt I'll be able to
write it myself in a reasonable time!Cheers,
Nicolas
Hi Nicolas,
I believe the $b parameter is missing from the first code block on the
Transformation Rules's section.
Good luck with the RFC, I hope it passes.
Daniel Vargas Muccillo