Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
Regards,
Máté Kocsis
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
Hey Máté,
How about just allowing a block of code after the clone statement that
would execute it in the same context as the clone context, allowing to
modify once readonly variables.
Allows better flexibility compared with clone with syntax or clone metho:
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;
$this->reasonPhrase = $reasonPhrase;
};
Regards,
Alex
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
A solid RFC, thanks!
I agree with the discussion of Nicolas's alternative, which has its merits but also some possible limitations. My only major concern myself is the potential confusion between when to use : and when to use =>. Using the => version only for dynamic keys feels very clunky. In fact, I think the RFC is inconsistent on this front:
$object = clone $object with {"foo" => 1}; // the property name is a literal
$object = clone $object with {strtolower("FO") . "o" => 1}; // the property name is an expression
$object = clone $object with {PROPERTY_NAME => 1}; // the property name is a named constant
I would expect the first one to be a colon, not fat-arrow. Is that a typo?
Is there a technical reason why they can't just all use a colon? That would extend more nicely to supporting dynamic keys in the future for named arguments.
Hey Máté,
How about just allowing a block of code after the clone statement that
would execute it in the same context as the clone context, allowing to
modify once readonly variables.
Allows better flexibility compared with clone with syntax or clone metho:public function withStatus($code, $reasonPhrase = ''): Response { return clone $this { $this->statusCode = $code; $this->reasonPhrase = $reasonPhrase;
};
This is an interesting alternative. I don't think $this is that confusing, actually. Inside __clone(), $this always refers to the new object. This proposal would essentially be providing an additional __clone block that runs in the same context, viz, $this always refers to the new object. I'm not sure if you need the original object separately at all, in fact; if you want to look up a value from the original object... you already have it on the new object in the same location, because it's a clone. So:
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;
$this->reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
}
};
$this->reasonPhrase in the string refers to the cloned object, where that value is by definition identical to the original object. It's evaluated, the string is computed, and then the clone's reasonPhrase property is updated.
The catch here is 2 fold:
-
This would essentially imply auto-capture. While I am quite OK with that, a fair number of people are not. But adding a use() block to that syntax just makes it very clunky for the obvious common case; which is basically the above, so it just forces you to repeat each variable name a third time. I would probably vote against something that forced more redundant use() statements.
-
The scope permissioning becomes weirder. The current RFC syntax is very predictable: properties get assigned from the context at which the clone statement lives, and if there are private/protected restrictions those are self-evident. With the more closure-like approach, it "feels" like you're providing an anon function that will get bound to the new object and then executed... which would then always run with private scope and be able to modify values you probably didn't want to be publicly modifiable. While we could potentially implement it such that the scope rules are still enforced, that may not be as obvious why you can/can't mess with a particular variable.
I'm open to this alternative though if Mate is; it has potential, but we'd have to sort out the above.
--Larry Garfield
Hi
I agree with the discussion of Nicolas's alternative, which has its merits but also some possible limitations. My only major concern myself is the potential confusion between when to use : and when to use =>. Using the => version only for dynamic keys feels very clunky. In fact, I think the RFC is inconsistent on this front:
$object = clone $object with {"foo" => 1}; // the property name is a literal
$object = clone $object with {strtolower("FO") . "o" => 1}; // the property name is an expression
$object = clone $object with {PROPERTY_NAME => 1}; // the property name is a named constantI would expect the first one to be a colon, not fat-arrow. Is that a typo?
No, that is correct. Colon takes a bare identifier, fat-arrow takes an
expression (that just happens to be a constant expression in that case).
Is there a technical reason why they can't just all use a colon? That would extend more nicely to supporting dynamic keys in the future for named arguments.
I'd rather see only the fat-arrow being allowed. Unless I'm missing
something, braces with colon is not used anywhere else, whereas braces +
'=>' is known from match() and '=>' more generally is already used with
array literals [1]. Having two similar syntaxes for the same thing is
not great when both are commonly needed is not great. They need to be
documented and learned by developers.
Best regards
Tim Düsterhus
[1] In fact if the right hand side of with may be an expression that
evaluates to an array, folks wouldn't need to learn new syntax at all:
$newProperties = [ "foo" => "bar" ];
clone $object with $newProperties;
and
clone $object with [ "foo" => "bar" ];
[1] In fact if the right hand side of with may be an expression that
evaluates to an array, folks wouldn't need to learn new syntax at all:$newProperties = [ "foo" => "bar" ]; clone $object with $newProperties;
and
clone $object with [ "foo" => "bar" ];
in my opinion this is sick, as in awesome. the {} version makes no sense to
me unless the language gains $var = {} ; to skip $var = (object)[];
[1] In fact if the right hand side of with may be an expression that
evaluates to an array, folks wouldn't need to learn new syntax at all:$newProperties = [ "foo" => "bar" ]; clone $object with $newProperties;
and
clone $object with [ "foo" => "bar" ];
in my opinion this is sick, as in awesome. the {} version makes no sense to
me unless the language gains $var = {} ; to skip $var = (object)[];
I think the {...} is meant to be simply an "argument list", not a value.
But yes having an array here would add some flexibility. It would also
give us argument unpacking for free.
I wonder what this means for performance.
Will the expression always be evaluated as an array first, and then
applied, or can php do a "shortcut" where it internally treats it as
an argument list, even though the syntax implies array?
The different will be small, but it could become relevant if called
many times over.
-- Andreas
Hi
I wonder what this means for performance.
Will the expression always be evaluated as an array first, and then
applied, or can php do a "shortcut" where it internally treats it as
an argument list, even though the syntax implies array?
The different will be small, but it could become relevant if called
many times over.
I've said it before, but I'm going to say it again: Performance should
be low in the list of things that decide how a feature will look.
Opcache and the optimizer can always be improved in the future, syntax
cannot.
Of course if a feature is unacceptably slow, it is a non-starter. But in
this case I expect it to just be "there is optimization potential", as
creating an array with a handful of elements shouldn't be unacceptably
slow and also 'clone with' is likely going to make up only a very small
part of a request's entire processing.
Best regards
Tim Düsterhus
I'd rather see only the fat-arrow being allowed. Unless I'm missing something, braces with colon is not used anywhere else, whereas braces + '=>' is known from match() and '=>' more generally is already used with array literals [1]. Having two similar syntaxes for the same thing is not great when both are commonly needed is not great. They need to be documented and learned by developers.
I think it makes sense to have an unquoted form here, because the common case is that they are names which analysers can match statically to particular properties, not strings which will be analysed at runtime. There are plenty of places in the language where dynamic names are allowed, but we don't just use strings for the static case:
${'foo'} = 'bar'( constant('BAZ') )->{'quux'}();
More specifically, the "name: $value" syntax matches named parameters, and while you can use an array for that (via ... unpacking), we don't force users to do so.
In fact, the Future Scope of that RFC considered the opposite: using the colon syntax in arrays, along with a shorthand for pulling the key name from the variable name: https://wiki.php.net/rfc/named_params#shorthand_syntax_for_matching_parameter_and_variable_name
If I wanted to put these ideas into a general framework, I think one way to go about this would be as follows:
- Consider identifier: $expr as a shorthand for "identifier" => $expr.
- Consider :$variable as a shorthand for variable: $variable and thus "variable" => $variable.
That would give us:
$point = ['x' => $x, 'y' => $y, 'z' => $z];
$point = [x: $x, y: $y, z: $z];
$point = [:$x, :$y, :$z];
$point = new Point(...['x' => $x, 'y' => $y, 'z' => $z]);
$point = new Point(x: $x, y: $y, z: $z);
$point = new Point(:$x, :$y, :$z);
$point = clone $point with {'x' => $x, 'y' => $y, 'z' => $z};
$point = clone $point with {x: $x, y: $y, z: $z};
$point = clone $point with {:$x, :$y, :$z};
Rather than making everything use an array or array-like syntax, I would probably go the other way and scrap the special syntax for dynamic names, making the whole thing look like a function call, with support for array unpacking:
$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);
Regards,
--
Rowan Tommins
[IMSoP]
I'd rather see only the fat-arrow being allowed. Unless I'm missing something, braces with colon is not used anywhere else, whereas braces + '=>' is known from match() and '=>' more generally is already used with array literals [1]. Having two similar syntaxes for the same thing is not great when both are commonly needed is not great. They need to be documented and learned by developers.
I think it makes sense to have an unquoted form here, because the common case is that they are names which analysers can match statically to particular properties, not strings which will be analysed at runtime. There are plenty of places in the language where dynamic names are allowed, but we don't just use strings for the static case:
${'foo'} = 'bar'( constant('BAZ') )->{'quux'}();
More specifically, the "name: $value" syntax matches named parameters, and while you can use an array for that (via ... unpacking), we don't force users to do so.
In fact, the Future Scope of that RFC considered the opposite: using the colon syntax in arrays, along with a shorthand for pulling the key name from the variable name: https://wiki.php.net/rfc/named_params#shorthand_syntax_for_matching_parameter_and_variable_name
If I wanted to put these ideas into a general framework, I think one way to go about this would be as follows:
- Consider identifier: $expr as a shorthand for "identifier" => $expr.
- Consider :$variable as a shorthand for variable: $variable and thus "variable" => $variable.
That would give us:
$point = ['x' => $x, 'y' => $y, 'z' => $z];
$point = [x: $x, y: $y, z: $z];
$point = [:$x, :$y, :$z];$point = new Point(...['x' => $x, 'y' => $y, 'z' => $z]);
$point = new Point(x: $x, y: $y, z: $z);
$point = new Point(:$x, :$y, :$z);$point = clone $point with {'x' => $x, 'y' => $y, 'z' => $z};
$point = clone $point with {x: $x, y: $y, z: $z};
$point = clone $point with {:$x, :$y, :$z};Rather than making everything use an array or array-like syntax, I would probably go the other way and scrap the special syntax for dynamic names, making the whole thing look like a function call, with support for array unpacking:
$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);Regards,
--
Rowan Tommins
[IMSoP]--
To unsubscribe, visit: https://www.php.net/unsub.php
I think using arrays here makes a lot of sense.
For example, this example:
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;
$this->reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
}
};
could be rewritten with
public function withStatus($statusCode, $reasonPhrase = ''): Response {
// perform validation here
$reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
return clone $this with compact('statusCode', 'reasonPhrase');
}
I, personally, would find this much more ergonomic than writing out
blocks of code and having a totally different syntax.
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;
$this->reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
}
};
Note that this is not the current proposed syntax. Since the keys are
not dynamic, the current proposal is this:
public function withStatus($statusCode, $reasonPhrase = ''): Response {
// perform validation here
$reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
return clone $this with {statusCode: $statusCode, reasonPhrase: $reasonPhrase};
}
public function withStatus($statusCode, $reasonPhrase = ''): Response {
// perform validation here
$reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
return clone $this with compact('statusCode', 'reasonPhrase');
}
The compact()
function always feels like a relic of the same era as
create_function() and call_user_func()
, both of which now have dedicated
syntax.
That's what Nikita was talking about in the RFC section I quoted
earlier: that compact('foo', 'bar') could be replaced with a dedicated
syntax like [:$foo, :$bar]
So if we insisted on arrays, that would be:
public function withStatus($statusCode, $reasonPhrase = ''): Response {
// perform validation here
$reasonPhrase = "Old: $this->reasonPhrase, New: $reasonPhrase";
return clone $this with [:$statusCode, :$reasonPhrase];
}
But I still don't see why an array should be the default case here,
rather than using ... to unpack one if you really need to, like we do
with arguments.
public function withStatus($statusCode, $reasonPhrase = null): Response {
$newProps = [:$statusCode];
if ( $reasonPhrase !== null ) {
$newProps['reasonPhrase'] = "Old: $this->reasonPhrase, New: $reasonPhrase";
}
return clone $this with (...$newProps);
}
Regards,
--
Rowan Tommins
[IMSoP]
Rather than making everything use an array or array-like syntax, I would probably go the other way and scrap the special syntax for dynamic names, making the whole thing look like a function call, with support for array unpacking:
$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);
or $point = clone($point, x: $x, y: $y, z: $z);
Also, I didn't see it mentioned, but maybe for future scope, these new
arguments should be passed to __clone().
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Rather than making everything use an array or array-like syntax, I would probably go the other way and scrap the special syntax for dynamic names, making the whole thing look like a function call, with support for array unpacking:
$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);or $point = clone($point, x: $x, y: $y, z: $z);
Also, I didn't see it mentioned, but maybe for future scope, these new
arguments should be passed to __clone().
They should not. See https://peakd.com/hive-168588/@crell/object-properties-part-2-examples . I went through and experimented with different syntaxes, and passing arguments to __clone() was by far the worst option in practice. :-)
--Larry Garfield
or $point = clone($point, x: $x, y: $y, z: $z);
Also, I didn't see it mentioned, but maybe for future scope, these new
arguments should be passed to __clone().They should not. See https://peakd.com/hive-168588/@crell/object-properties-part-2-examples . I went through and experimented with different syntaxes, and passing arguments to __clone() was by far the worst option in practice. :-)
Sure, I didn't propose it to be a solution for "clone with", but rather,
if we implement "clone with" (no matter in which way) this is "cloning
an object with extra properties", so therefore one would ask to have
access to these extra properties inside __clone(). I'm not sure how
useful that could be, but maybe worth mentioning in future scope or
somewhere in the RFC.
And I think this syntax should still be on the table, no need for a new
keyword.
$point = clone($point, x: $x, y: $y, z: $z);
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Rather than making everything use an array or array-like syntax, I
would probably go the other way and scrap the special syntax for
dynamic names, making the whole thing look like a function call, with
support for array unpacking:$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);
I agree here, for all the reasons Rowan indicated. We already have a perfectly good syntax and semantics for named arguments that supports splat. Using that here would handle all the use cases we care about, including dynamic names, without any additional syntax.
Let's not shoe-horn arrays in here where they're not needed. Making arrays themselves fancier/more compact with an alternate syntax is worth discussing, but that should be a separate RFC.
--Larry Garfield
Hi
Rather than making everything use an array or array-like syntax, I
would probably go the other way and scrap the special syntax for
dynamic names, making the whole thing look like a function call, with
support for array unpacking:$point = clone $point with (x: $x, y: $y, z: $z);
$point = clone $point with (...['x' => $x, 'y' => $y, 'z' => $z]);I agree here, for all the reasons Rowan indicated. We already have a perfectly good syntax and semantics for named arguments that supports splat. Using that here would handle all the use cases we care about, including dynamic names, without any additional syntax.
FWIW I'm not too attached to my array proposal [1]. I would also be fine
with Rowan's proposal of making the "with()" syntactically identical to
a function call, if that's more agreeable.
But please no entirely new syntax with braces as it currently is shown
in the examples in the RFC.
Best regards
Tim Düsterhus
[1] Though I still consider arrays to be more "natural" than named
arguments.
Hi Tim,
czw., 20 kwi 2023 o 16:39 Tim Düsterhus tim@bastelstu.be napisał(a):
...
But please no entirely new syntax with braces as it currently is shown
in the examples in the RFC.
Then we should vote for syntax. Personally, I prefer braces here
because it doesn't look like a regular function call allowing easily to
distinguish
between two different things.
Cheers,
Michał Marcin Brzuchalski
Hey Everyone,
Thank you for the lot of feedback! Sorry, I'm going to have to answer in a
single email otherwise I would have to send too many emails.
Alexandru wrote:
How about just allowing a block of code after the clone statement that
would execute it in the same context as the clone context, allowing to
modify once readonly variables.
Allows better flexibility compared with clone with syntax or clone method:
This is an interesting alternative, but as Tim pointed out in a later
reply, the block would run in the private scope, which is contrary to my
intentions (and what I consider
the best solution).
Michał wrote:
I am curious if possible to implement the feature without using with
keyword
it visually could look pretty close to something like an object
initializer in the future:
return clone $this {c: 1};
return new Bar {c: 1};
That's exactly what I tried first, but unfortunately, this syntax led to
parser ambiguities, so at last I had to settle on using "with".
Rowan wrote:
- You mention in the Alternatives sometimes needing access to the
original instance; it would be good to have an example of how this looks
with the clone-with syntax.
Makes sense, so I've just come up with an example where a linked list of
objects are created. Let me know if you have a better example :)
- How does this interact with an __clone() method? I'm guessing the
__clone() would be called first, and then the with-clause applied?
Yeah, thanks for pointing this out! I agree that the clarification is very
much needed. BTW the evaluation order
is exactly how you wrote. This is now added to the RFC.
Tim wrote:
In which order will __clone(), foo(), bar(), quux(), baz() be evaluated
and what will happen if one of them throws an Exception? Will
destructors of the cloned object run? When?
In fact, after the initial ZEND_CLONE opcode (which possibly invokes
__clone()), a separate opcode is generated for each
assignment (the newly added ZEND_CLONE_INIT_PROP). This means that foo(),
bar(), quux(), and baz() will be evaluated
in this very order. If any of them throws an exception, evaluation stops
and the assignments are not rolled back.
Regarding the destructors: yes, the destructor of the cloned object runs
immediately. In order to make sure, I've just added a test case:
https://github.com/php/php-src/pull/9497/commits/4d184f960ac1b5590d87739ee3278c13fac157de
I hope that this result is what
you expect.
Michal wrote:
Just noticed the "Property name expressions" and am wondering if it could
be a separate feature
allowing for passing named arguments to functions/constructors in the same
fashion?
As far as I can see, Nikita didn't propose the expression1() =>
expression2() syntax in the named params RFC due to
the same ambiguity I mentioned in the RFC (identifier vs. global constant).
But I don't think this is set in stone, however,
I do think that some optimizations would have to be disabled when param
names weren't evaluatable in
compile-time (https://github.com/php/php-src/pull/10831).
Andreas wrote:
What about argument unpacking?
I don't know if we can combine this with ":" syntax or only with "=>".
For now, argument unpacking (property unpacking?) is not possible. But it
is definitely something that could be added in the future.
Tim wrote:
I'd rather see only the fat-arrow being allowed. Unless I'm missing
something, braces with colon is not used anywhere else, whereas braces +
'=>' is known from match() and '=>' more generally is already used with
array literals [1]. Having two similar syntaxes for the same thing is
not great when both are commonly needed is not great. They need to be
documented and learned by developers.
I can only repeat what Rowan answered, since I agree with it completely:
I think it makes sense to have an unquoted form here, because the common
case is that they are names which analysers can match statically to
particular properties, not strings which will be analysed at runtime. There
are plenty of places in the language where dynamic names are allowed, but
we don't just use strings for the static case
However, I'm not completely sold on making "clone with" look like a
function call (return clone $this with (a: 1);), but
at least I like it more than using an array-like style (return clone $this
with [a: 1];). My intention with
using curly brackets (return clone $this with {a: 1};) is to highlight the
fact that it is a map
of key-value pairs, similarly how the JSON standard does so. Not to mention
that "clone with" serves a very
similar purpose to object initializers, and the different languages I know
to have this feature use
a similar syntax (Java: http://wiki.c2.com/?DoubleBraceInitialization, C#:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#object-initializers
).
Regards,
Máté
What about argument unpacking?
I don't know if we can combine this with ":" syntax or only with "=>".
For now, argument unpacking (property unpacking?) is not possible. But it
is definitely something that could be added in the future.
Not currently written but could be, or there's some reason why that is particularly hard?
Tim wrote:
I'd rather see only the fat-arrow being allowed. Unless I'm missing
something, braces with colon is not used anywhere else, whereas braces +
'=>' is known from match() and '=>' more generally is already used with
array literals [1]. Having two similar syntaxes for the same thing is
not great when both are commonly needed is not great. They need to be
documented and learned by developers.I can only repeat what Rowan answered, since I agree with it completely:
I think it makes sense to have an unquoted form here, because the common
case is that they are names which analysers can match statically to
particular properties, not strings which will be analysed at runtime. There
are plenty of places in the language where dynamic names are allowed, but
we don't just use strings for the static caseHowever, I'm not completely sold on making "clone with" look like a
function call (return clone $this with (a: 1);), but
at least I like it more than using an array-like style (return clone
$this
with [a: 1];). My intention with
using curly brackets (return clone $this with {a: 1};) is to highlight
the
fact that it is a map
of key-value pairs, similarly how the JSON standard does so. Not to
mention
that "clone with" serves a very
similar purpose to object initializers, and the different languages I
know
to have this feature use
a similar syntax (Java: http://wiki.c2.com/?DoubleBraceInitialization,
C#:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#object-initializers
).Regards,
Máté
True, but PHP 8.0 sort of added something close to object initializers with the combination of constructor promotion and named arguments. It's not exactly the same thing, but it's the effective equivalent in PHP today. So if you want to model on object initializers, named args would be the closest PHP equivalent.
--Larry Garfield
Hi
- How does this interact with an __clone() method? I'm guessing the
__clone() would be called first, and then the with-clause applied?
Yeah, thanks for pointing this out! I agree that the clarification is very
much needed. BTW the evaluation order
is exactly how you wrote. This is now added to the RFC.Tim wrote:
In which order will __clone(), foo(), bar(), quux(), baz() be evaluated
and what will happen if one of them throws an Exception? Will
destructors of the cloned object run? When?In fact, after the initial ZEND_CLONE opcode (which possibly invokes
__clone()), a separate opcode is generated for each
assignment (the newly added ZEND_CLONE_INIT_PROP). This means that foo(),
bar(), quux(), and baz() will be evaluated
in this very order. If any of them throws an exception, evaluation stops
and the assignments are not rolled back.Regarding the destructors: yes, the destructor of the cloned object runs
immediately. In order to make sure, I've just added a test case:
https://github.com/php/php-src/pull/9497/commits/4d184f960ac1b5590d87739ee3278c13fac157de
I hope that this result is what
you expect.
I'm combining your two replies, as they are related: The behavior is
what I expected. It would however be useful if you also added an
explicit example with side effects to "Interaction with the __clone
magic method" section in the RFC and the tests (just 'echo FUNCTION'
or so) to make it explicit. I believe the attached example should do the
trick. If I understand correctly the output should be:
__clone
a
b
c
d
unhandled exception
__destruct
__destruct
I'd rather see only the fat-arrow being allowed. Unless I'm missing
something, braces with colon is not used anywhere else, whereas braces +
'=>' is known from match() and '=>' more generally is already used with
array literals [1]. Having two similar syntaxes for the same thing is
not great when both are commonly needed is not great. They need to be
documented and learned by developers.I can only repeat what Rowan answered, since I agree with it completely:
I think it makes sense to have an unquoted form here, because the common
case is that they are names which analysers can match statically to
particular properties, not strings which will be analysed at runtime. There
are plenty of places in the language where dynamic names are allowed, but
we don't just use strings for the static case
A static analyzer should be able to understand a string literal.
However, I'm not completely sold on making "clone with" look like a
function call (return clone $this with (a: 1);), but
at least I like it more than using an array-like style (return clone $this
with [a: 1];). My intention with
using curly brackets (return clone $this with {a: 1};) is to highlight the
fact that it is a map
of key-value pairs, similarly how the JSON standard does so. Not to mention
To a PHP programmer, a map of arbitrary key-value pairs is an array.
That's what is familiar to every PHP programmer and thus an array
literal should feel reasonably natural and obvious with regard to
semantics (that's why it was my suggestion).
The named parameters syntax is fairly new, but at least there is
precedent and thus developers are able to transfer their existing knowledge.
Both would be able to completely replace the Foo::withProperties()
example by means of existing syntax (either bare array or named
parameter destructuring) and without the overhead of repeated cloning.
The brace + colon syntax is a completely new invention and less flexible
as shown by the need to use a different separator character to
differentiate between bare names and global constants.
that "clone with" serves a very
similar purpose to object initializers, and the different languages I know
to have this feature use
a similar syntax (Java: http://wiki.c2.com/?DoubleBraceInitialization, C#:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#object-initializers
).
Different languages, different syntax choices and different existing
syntax. I don't do C#, but I'm sure that the C# syntax totally makes
sense in the context of C#. I do not believe it makes sense in PHP.
Best regards
Tim Düsterhus
Hi Máté,
pon., 17 kwi 2023 o 08:32 Máté Kocsis kocsismate90@gmail.com napisał(a):
Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour
section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
Thanks for your efforts and for bringing that up.
I am curious if possible to implement the feature without using with
keyword
it visually could look pretty close to something like an object initializer
in the future:
return clone $this {c: 1};
return new Bar {c: 1};
Cheers,
Michał Marcin Brzuchalski
Hey,
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;$this->reasonPhrase = $reasonPhrase; }
};
How to refer to any of the properties of the current instance? Let's say
this:
class Foo {
protected int $code;
protected string $message;
public function withStatus($code, $message): Foo {
return clone $this {
$this->code = $code; // so far so good
// Which $this->message is what?
$this->message = "cloned: (" . $this->message . ")" . $message;
}
}
}
Thanks for your efforts and for bringing that up.
I am curious if possible to implement the feature without usingwith
keyword
it visually could look pretty close to something like an object
initializer
in the future:return clone $this {c: 1};
return new Bar {c: 1};
Similarity is not necessarily always good.
Regards,
Zoltán Fekete
Hi Zoltán,
On Mon, Apr 17, 2023 at 11:13 PM Zoltán Fekete fekzol.13ker@gmail.com
wrote:
Hey,
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone $this {
$this->statusCode = $code;
$this->reasonPhrase = $reasonPhrase;
};
}How to refer to any of the properties of the current instance? Let's say
this:class Foo { protected int $code; protected string $message; public function withStatus($code, $message): Foo { return clone $this { $this->code = $code; // so far so good // Which $this->message is what? $this->message = "cloned: (" . $this->message . ")" . $message; } } }
Yes, it's true that it looks weird that $this is present in two places with
two different meanings, but that's just because we are cloning $this. If we
were cloning $object, it would be clearer.
$this inside the braces is referring to the new object after it was cloned,
after __clone() was executed, if defined.
The old instance data is already copied over, no real need to reference
that.
Maybe we can think about it a bit more and improve the idea. I'm
thinking we can have a closure/callable or an instance method reference
instead of a block of code.
Also, that might be preferable, as there is a natural way to either use
closure with "use" or pass parameters.
I don't like too much how $code and $message are referenced inside the
block of code... maybe it's not an issue.
Also, I'm not really sure how easy it would be to model this behavior, to
have a block of code where $this would reference something else than
outside of it.
Regards,
Alex
I look the look of this; very concise.
A couple of clarifications that might be useful to add to the RFC:
-
You mention in the Alternatives sometimes needing access to the
original instance; it would be good to have an example of how this looks
with the clone-with syntax. -
How does this interact with an __clone() method? I'm guessing the
__clone() would be called first, and then the with-clause applied?
Regards,
--
Rowan Tommins
[IMSoP]
Hi
- How does this interact with an __clone() method? I'm guessing the
__clone() would be called first, and then the with-clause applied?
More generally the order of operations with regard to possible side
effects and/or exceptions would be interesting.
clone $something with {
foo() => bar(),
quux() => baz(),
};
In which order will __clone(), foo(), bar(), quux(), baz() be evaluated
and what will happen if one of them throws an Exception? Will
destructors of the cloned object run? When?
Best regards
Tim Düsterhus
wt., 18 kwi 2023 o 10:20 Tim Düsterhus tim@bastelstu.be napisał(a):
Hi
- How does this interact with an __clone() method? I'm guessing the
__clone() would be called first, and then the with-clause applied?More generally the order of operations with regard to possible side
effects and/or exceptions would be interesting.clone $something with { foo() => bar(), quux() => baz(), };
Just noticed the "Property name expressions" and am wondering if it could
be a separate feature
allowing for passing named arguments to functions/constructors in the same
fashion?
$something = new Something(
foo() => bar(),
quux() => baz(),
);
Cheers,
Michał Marcin Brzuchalski
Hello Máté, internals,
I have been waiting for this to happen :)
Some feedback
However, in some cases it would be useful to reference property names as expressions, e.g. when one needs to use “clone with” in a foreach loop where the index is the property name and the loop variable is the value to be assigned. This is also possible using a slightly more verbose syntax:
What about argument unpacking?
I don't know if we can combine this with ":" syntax or only with "=>".
clone $object with {
...$arr,
...['a' => 'A', $b => 'B'],
...(function () {yield 'x' => 'X';})(),
c: 'C', // or
d => 'D', // No mixing of ':' and '=>', we have to choose one.
}
If we want to go crazy in the future:
(This would require another language feature of inline code blocks
with return value or behaving as generator)
clone $object with {
...{
yield 'a' => 'A';
yield 'b' => 'B';
},
...{
return ['c' => 'C'],
}
}
-- Andreas
Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_withRegards,
Máté Kocsis
The biggest advantage of Nicolas' proposal over “clone with” is that it could be made part of the interface contract whether a method modifies the object state. So the PSR-7 ResponseInterface could look like the following:
[..]
public clone function withStatus($code, $reasonPhrase = '');
If this is the main argument, I think I would prefer a more general
"readonly" modifier on methods.
public readonly function withStatus($code, $reasonPhrase = ''): static;
One cannot control whether $this should really be cloned: e.g. if a property should only be modified based on certain conditions (e.g. validation), the object would potentially be cloned in vain, resulting in a performance loss.
Exactly. I would say "conditionally clone", e.g. only clone if there
is a change, or if we don't already have a cached instance with this
value.
I think the "clone with" is much more flexible because we can call
this anywhere, not just in a dedicated method.
This said, I do agree with some of the benefits of the "Alternative" proposal.
-- Andreas
Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_withRegards,
Máté Kocsis
Hi Mate,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour
section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
Thanks for working on this, we definitely need improvements on the topic.
Thanks also for mentioning my proposal and for the comparison analysis,
that's really helpful.
Quoting from the RFC:
One cannot control whether $this should really be cloned: e.g. if a
property should only be modified based on certain conditions (e.g.
validation), the object would potentially be cloned in vain, resulting in a
performance loss.
Returning a clone or the same instance depending e.g. on some validation is
usually an abstraction leak. It's a leak because it allows knowing internal
implementation details by comparing the identity of the resulting objects.
Since the PHP community is leaning towards more strictness and better
abstractions, I think this point is actually a win for my proposal: it'd
naturally close the loophole in wither-based abstractions. See
https://3v4l.org/02IVc#v8.2.5 if what I mean is unclear.
Sometimes one also needs access to the original instance, but doing so
would only be possible via workarounds (e.g. by introducing $that or a
similar construct).
Here is how we would achieve this under my proposal (adapting from your
example):
class LinkedObject {
/.../
public function next()
: static {
$clone = $this->prepareNext();
$clone->next = $this;
return $clone;
}
private clone function prepareNext(): static {
$this->number++;
unset($this->next);
return $this;
}
}
As you can see, this works by not using the clone keyword on the public
method. And this makes me realize that what you see as an advantage ("it
could be made part of the interface contract") might actually be a
drawback, by forcing a coupling between a contract and an implementation.
That's what you mean also with your last sentence on your analyses.
And that kills my proposal, RIP :)
But I have another proposal I'm sending separately.
Nicolas
Hi again,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour
section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
As mentioned in another thread, I'd like to make an alternative proposal
for the syntax.
Alex talked about it already but I think it deserves more attention.
What about using a real closure to define the scope we need for cloning?
That closure would take the cloned instance as argument to allow
manipulating it at will.
Internally, the engine would "just" call that closure just after calling
__clone() if it's defined.
This could look like this:
$c = clone $a with $closure;
or maybe we could skip introducing a new keyword and go for something that
looks like a function call:
$c = clone($a, $closure);
I've adapted your examples from the RFC using the latter and here is how
they could look like:
The “wither” method copy-pasted from Diactoros:
class Response implements ResponseInterface {
public readonly int $statusCode;
public readonly string $reasonPhrase;
// ...
public function withStatus($code, $reasonPhrase = ''): Response
{
return clone($this, fn ($clone) => [
$clone->statusCode = $code,
$clone->reasonPhrase = $reasonPhrase,
]);
}
// ...
}
The property name expressions:
class Foo {
private $a;
private $b;
private $c;
/**
- @param array<string, mixed> $properties
*/
public function withProperties(array $properties) {
return clone($this, function ($clone) use ($properties) {
foreach ($properties as $name => $value) {
$clone->$name = $value;
}
});
}
}
Linking the cloned instance to the original one:
class LinkedObject {
public function __construct(
private readonly LinkedObject $next,
private readonly int $number
) {
$this->number = 1;
}
public function next()
: LinkedObject {
return clone($this, fn ($clone) => [
$clone->number = $clone->number + 1,
$this->next = $clone,
]);
}
}
All these look pretty neat to me, and they come with no new syntax but a
simple call-like clone() statement.
Scope semantics remain the same as usual, so we already know how to
interpret that aspect.
Does that make sense to you?
Nicolas
What about using a real closure to define the scope we need for cloning?
That closure would take the cloned instance as argument to allow
manipulating it at will.
I believe someone mentioned that one previously in the thread. The problem is that the closure would run in the scope of the object, not the scope of the caller. That means if called outside the object, it would allow modifying private or protected properties. The itemized list of values (whether an array or named-args style) would allow the engine to enforce access restrictions, which is a desireable feature.
--Larry Garfield
On Wed, Apr 26, 2023 at 3:25 AM Larry Garfield larry@garfieldtech.com
wrote:
What about using a real closure to define the scope we need for cloning?
That closure would take the cloned instance as argument to allow
manipulating it at will.I believe someone mentioned that one previously in the thread.
Yes, Nicolas mentioned me.
I wanted to get back myself to discussing this topic more as well and find
a better solution but didn't get yet time for it.
The problem is that the closure would run in the scope of the object,
not the scope of the caller. That means if called outside the object, it
would allow modifying private or protected properties. The itemized list
of values (whether an array or named-args style) would allow the engine to
enforce access restrictions, which is a desireable feature.
As far as I can see, Nicolas was able to find a solution to this problem
and so far I like it:
The closure is running in the current scope where it is defined and not in
the scope of the cloned object. The cloned object is passed as the single
parameter to the closure.
The suggested clone signature for a class T would be:
- clone(T $object, callable(T $clone)): T; // calling clone as a function
- clone T $object with callable(T $clone): T; // calling clone as a
language construct
Alternatively, we can have also: - clone T, callable(T $clone); // without "with" keyword
And improve it to allow even multiple closures to be executed: - clone(T $object, ...callable(T $clone)): T;
- clone T $object, ...callable(T $clone): T;
IMHO, I think we should support both syntaxes.
The main reason I wanted to get back to this proposal is because > 90% of a
developer's job is to read code and the most trouble is following state
modifications of a variable.
When that variable to be investigated is a property, so far it was simple
to look for "->propertyName = <rhs expression>".
Editors would easily and clearly highlight all the write access statements
or lines (Alt+F7 in PhpStorm). I can't figure out what would be highlighted
in the non-closure syntax, especially the one with an array.
Thanks,
Alex
What about using a real closure to define the scope we need for
cloning?
That closure would take the cloned instance as argument to allow
manipulating it at will.I believe someone mentioned that one previously in the thread.
Yes, Nicolas mentioned me.
I wanted to get back myself to discussing this topic more as well and find
a better solution but didn't get yet time for it.The problem is that the closure would run in the scope of the object,
not the scope of the caller. That means if called outside the object, it
would allow modifying private or protected properties. The itemized list
of values (whether an array or named-args style) would allow the engine
to
enforce access restrictions, which is a desireable feature.As far as I can see, Nicolas was able to find a solution to this problem
and so far I like it:
The closure is running in the current scope where it is defined and not in
the scope of the cloned object. The cloned object is passed as the single
parameter to the closure.
Absolutely. This would be a plain boring closure with all its current
visibility semantics.
Using a closure to run some code nested in a transaction is already quite a
common practice.
E.g.this is a common way to define the computation logic for a cache
storage:
$cache->get($cacheKey, $callback)
Or for a database transaction:
$db->transaction(function() { ... });
The cloning logic we want to run fits this style, so this is quite natural
once we realize that:
$clone = clone($this, $callback);
The only thing we would need to settle on is the interface of the $callback.
The suggested clone signature for a class T would be:
- clone(T $object, callable(T $clone)): T; // calling clone as a function
- clone T $object with callable(T $clone): T; // calling clone as a
language construct
100% this. My preference goes for #1, to keep things as boring as possible.
Just to make it clear, I would document the closure with the void return
type:
clone(T $object, callable(T $clone):void): T; // calling clone as a
function
Alternatively, we can have also:
- clone T, callable(T $clone); // without "with" keyword
This would be ambiguous, eg foo(clone T, callable(T $clone)) is that a
function call with two arguments, or?
And improve it to allow even multiple closures to be executed:
- clone(T $object, ...callable(T $clone)): T;
- clone T $object, ...callable(T $clone): T;
I wouldn't allow this. Calling many closures is what the main closure can
do in its body, no need for more fancy things.
Nicolas
Hi Everyone,
In the meanwhile, I changed my proposal to use [] instead of {} after the
"with" clause due to its better receptance.
Additionally, I removed support for the shorthand "property assignment"
syntax (clone $this with [property1: "foo"]) in
favor the more powerful one where the left-hand side supports expressions
(clone $this with ["property1" => "foo"])
so that we have more time to decide whether the former one is really needed.
After talking with Nicolas and Alexandru off-list (I accidentally didn't
click "Reply All" in my response), we identified
that their "clone callback" proposal could also be a future-scope of the
current RFC with the following syntax:
clone $this with function (object $clone): void {
$this->property1 = "foo";
}
In my opinion, this syntax is OK, albeit it's a bit long to write and a bit
difficult to decipher its meaning (maybe it's
just me). That's the main reason I prefer my originally proposed syntax.
Apart from this, I don't see a big issue with it,
neither conceptually, nor with the implementation.
Regards,
Máté
Hi Máté,
pon., 29 maj 2023 o 11:18 Máté Kocsis kocsismate90@gmail.com napisał(a):
Hi Everyone,
In the meanwhile, I changed my proposal to use [] instead of {} after the
"with" clause due to its better receptance.
Additionally, I removed support for the shorthand "property assignment"
syntax (clone $this with [property1: "foo"]) in
favor the more powerful one where the left-hand side supports expressions
(clone $this with ["property1" => "foo"])
so that we have more time to decide whether the former one is really
needed.
So there would be no option to vote on shorthand properties, right?
Array syntax with all properties as strings in quotes probably means no
support from IDE.
I think this is a really bad move, I'd be against it.
Cheers,
Michał Macin Brzuchalski
Hi Máté,
pon., 29 maj 2023 o 11:18 Máté Kocsis kocsismate90@gmail.com napisał(a):
Hi Everyone,
In the meanwhile, I changed my proposal to use [] instead of {} after the
"with" clause due to its better receptance.
Additionally, I removed support for the shorthand "property assignment"
syntax (clone $this with [property1: "foo"]) in
favor the more powerful one where the left-hand side supports expressions
(clone $this with ["property1" => "foo"])
so that we have more time to decide whether the former one is really
needed.So there would be no option to vote on shorthand properties, right?
Array syntax with all properties as strings in quotes probably means no
support from IDE.
I think this is a really bad move, I'd be against it.
I concur, especially with a dynamic list pushed off to future scope. As is, we're getting less IDE friendliness and a lot more ' characters in return for making the property name dynamic. However, the number of property names is still fixed at code time. That's an incomplete tradeoff.
Conversely, using the named arguments style syntax (what future scope calls a shorthand, which I don't think is accurate as it's nearly the same amount of typing) and allowing it to be "splatted", just like named arguments, gives us the best of all worlds. The typical case is easy to read, easy to write, and IDE-refactor-friendly. Allowing it to be splatted (just like named arguments) means that in the unusual case where you want the number or names of the properties to be dynamic (which is a valid use case, but a minority one), you can pre-build an array and splat it in place, exactly like you can named arguments.
Typical:
return clone $this with (
statusCode: $code,
);
Fancy:
$props[strtolower('statusCode')] = $code;
if ($reason) {
$props['reason'] = $reason;
}
return clone $this with ...$props;
That approach, taken now, gives us all the flexibility people have been asking for, reuses the existing named arguments syntax entirely, and is the most refactorable option.
Additionally, the current "property names expression" section shows a very odd example. It's recloning the object N times, reinvoking the __clone method each time, and reassigning a single property. If you're updating several properties at once, that is extremely inefficient. Dynamic property names are not the solution there; allowing the list to be dynamic in the first place is, and that should not be punted to future scope.
I really want clone-with, but this seems like a big step backwards in terms of its capability. It may be enough for me to vote no on it, but I'm not sure.
(Pushing clone-with-callable off to later is, I agree, the correct move.)
I am also confused by the choice to make __clone() run before with with properties. I would have expected it to be the other way around. Can you explain (in the RFC) why you did it that way? That seems more limiting than the alternative, as you cannot forward-pass information to __clone() this way.
--Larry Garfield
Hi Michał and Larry,
As Tim has already clarified, using literal strings in the left-hand side
of "clone with expressions" won't cause any issues
for IDEs and static analysers to identify the correct property. I got rid
of the shorthand syntax from the proposal because
it is not strictly required (since it has an equivalent alternative), and I
acknowledge the fact that there are certain people
who don't want to have two syntaxes to express the same thing. If it turns
out that we really want to have the short-hand
syntax as well, we can always add support for it later.
Conversely, using the named arguments style syntax (what future scope calls
a shorthand, which I don't think is accurate as it's nearly the same amount
of typing)
I'm just using the same wording as Nikita used in his named argument RFC.
Please take a look at the following section:
https://wiki.php.net/rfc/named_params#shorthand_syntax_for_matching_parameter_and_variable_name
Especially this sentence is important:
If I wanted to put these ideas into a general framework, I think one way to
go about this would be as follows:
- Consider identifier: $expr as a shorthand for "identifier" => $expr.
Even though the shorthand syntax is only 4 characters shorter than the
normal one (including the missing space before the colon),
for a typical property name which may be 4-8 characters long (I guess),
it's a significant reduction of typing.
That approach, taken now, gives us all the flexibility people have been
asking for, reuses the existing named arguments syntax entirely, and is the
most refactorable option.
Your suggestion is very interesting/promising... However, I'm a bit worried
that if I only supported the "identifier: expression" syntax for
"clone assignments", and I required people to pass an array (or splatted
array) instead of the "expression => expression" syntax,
they would start complaining why they cannot directly use expressions as
property names... That's why I think all the alternatives are useful
on their own, but only the "expressions => expression" syntax is
vital, since all the other use-cases can be written like this.
Additionally, the current "property names expression" section shows a very
odd example. It's recloning the object N times, reinvoking the __clone
method each time, and reassigning a single property. If you're updating
several properties at once, that is extremely inefficient. Dynamic
property names are not the solution there; allowing the list to be dynamic
in the first place is, and that should not be punted to future scope.
I'm aware that it's currently inefficient, but it's the case only when you
want to update a dynamic list of properties. In my opinion, you update
a fixed set of properties most of the time
(e.g. Psr\Http\Message\ResponseInterface::withStatus()), and it's only a
rare case when you need
more dynamic behavior. That's why I believe it's not a huge problem to keep
cloning objects unnecessarily until we add support for the stuff
mentioned in the future scope section.
I am also confused by the choice to make __clone() run before with with
properties. I would have expected it to be the other way around. Can you
explain (in the RFC) why you did it that way? That seems more limiting
than the alternative, as you cannot forward-pass information to __clone()
this way.
To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after the
clone assignments.
Do you have a use-case in mind when you need to forward-pass information to
__clone()?
Regards,
Máté
Hi Michał and Larry,
As Tim has already clarified, using literal strings in the left-hand side
of "clone with expressions" won't cause any issues
for IDEs and static analysers to identify the correct property. I got rid
of the shorthand syntax from the proposal because
it is not strictly required (since it has an equivalent alternative), and I
acknowledge the fact that there are certain people
who don't want to have two syntaxes to express the same thing. If it turns
out that we really want to have the short-hand
syntax as well, we can always add support for it later.
My point is, rather, that if we want to avoid having multiple syntaxes (which is valid), the existing named-args syntax and behavior already more than adequately covers all bases.
That approach, taken now, gives us all the flexibility people have been
asking for, reuses the existing named arguments syntax entirely, and is the
most refactorable option.Your suggestion is very interesting/promising... However, I'm a bit worried
that if I only supported the "identifier: expression" syntax for
"clone assignments", and I required people to pass an array (or splatted
array) instead of the "expression => expression" syntax,
they would start complaining why they cannot directly use expressions as
property names... That's why I think all the alternatives are useful
on their own, but only the "expressions => expression" syntax is
vital, since all the other use-cases can be written like this.
Has the "if you need a variable name/number, use an array" been a problem for named arguments? I've not seen it been one. Has anyone else?
You cannot use an expression as a function argument name today, and... no one seems to mind. Using an array in that rare edge case is fine.
I think we agree that the most common case by far will be a fixed, small set of statically-named properties. So we should optimize for that case, which is the named-args style.
Additionally, the current "property names expression" section shows a very
odd example. It's recloning the object N times, reinvoking the __clone
method each time, and reassigning a single property. If you're updating
several properties at once, that is extremely inefficient. Dynamic
property names are not the solution there; allowing the list to be dynamic
in the first place is, and that should not be punted to future scope.I'm aware that it's currently inefficient, but it's the case only when you
want to update a dynamic list of properties. In my opinion, you update
a fixed set of properties most of the time
(e.g. Psr\Http\Message\ResponseInterface::withStatus()), and it's only a
rare case when you need
more dynamic behavior. That's why I believe it's not a huge problem to keep
cloning objects unnecessarily until we add support for the stuff
mentioned in the future scope section.
Except that named-args style gives us the better alternative today, with known, pre-existing syntax and semantics.
I am also confused by the choice to make __clone() run before with with
properties. I would have expected it to be the other way around. Can you
explain (in the RFC) why you did it that way? That seems more limiting
than the alternative, as you cannot forward-pass information to __clone()
this way.To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after the
clone assignments.
Do you have a use-case in mind when you need to forward-pass information to
__clone()?
Not a specific one off hand. It's more a conceptual question. with
has more contextual awareness than __clone(), so it should have "first crack" at the operation, so that if necessary it can make changes that __clone() can then respond to. The inverse doesn't make sense.
The only reason for with
to come after would be to allow with
to "override" or "undo" something that __clone() did. Generally speaking, if you have to undo something you just did, you shouldn't have done it in the first place, so that's a less compelling combination.
This one isn't a deal breaker, but we should be sure to think it through as it's kinda hard to reverse later.
--Larry Garfield
Hi
To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after the
clone assignments.
Do you have a use-case in mind when you need to forward-pass information to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has more contextual awareness than __clone(), so it should have "first crack" at the operation, so that if necessary it can make changes that __clone() can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to "override" or "undo" something that __clone() did. Generally speaking, if you have to undo something you just did, you shouldn't have done it in the first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it through as it's kinda hard to reverse later.
FWIW if I would've create the implementation, I intuitively would also
have chosen to first execute __clone() and then set the properties. Of
course this isn't a well-argued reason to do it this way, but I wanted
to share it nonetheless.
Best regards
Tim Düsterhus
To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after
the
clone assignments.
Do you have a use-case in mind when you need to forward-pass information
to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has
more contextual awareness than __clone(), so it should have "first crack"
at the operation, so that if necessary it can make changes that __clone()
can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to
"override" or "undo" something that __clone() did. Generally speaking, if
you have to undo something you just did, you shouldn't have done it in the
first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it through
as it's kinda hard to reverse later.
To me so far also it was natural to assume that __clone is first and only
after that the rest of the operations.
But with
operations, be it properties assignment or even a closure, would
run in the context of the caller of clone and sometimes this might be run
not from a method of the cloned class.
An example:
There is a class that represents persons of a fictive country/planet.
Each person has many properties but has also a first name and a last name
and there is a rule: the two names must not start with the same letter.
Both names cannot be changed as they are defined readonly.
Creation of new persons can be done using new for new random properties or
using clone to preserve existing properties. But in both cases the first
name and last name are randomly chosen.
If we want to control the last name value during clone that would be
possible using the with
operation but the logic to allocate a first name
will only happen in __clone()
method.
To be able to achieve this we must have __clone last, as there we have the
internal validations, operations and also access to private/protected
members that are not accesible from where clone is being called.
Regards,
Alex
To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after
the
clone assignments.
Do you have a use-case in mind when you need to forward-pass information
to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has
more contextual awareness than __clone(), so it should have "first crack"
at the operation, so that if necessary it can make changes that __clone()
can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to
"override" or "undo" something that __clone() did. Generally speaking, if
you have to undo something you just did, you shouldn't have done it in the
first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it through
as it's kinda hard to reverse later.To me so far also it was natural to assume that __clone is first and only
after that the rest of the operations.
Butwith
operations, be it properties assignment or even a closure, would
run in the context of the caller of clone and sometimes this might be run
not from a method of the cloned class.An example:
There is a class that represents persons of a fictive country/planet.
Each person has many properties but has also a first name and a last name
and there is a rule: the two names must not start with the same letter.
Both names cannot be changed as they are defined readonly.
Creation of new persons can be done using new for new random properties or
using clone to preserve existing properties. But in both cases the first
name and last name are randomly chosen.
If we want to control the last name value during clone that would be
possible using thewith
operation but the logic to allocate a first name
will only happen in__clone()
method.To be able to achieve this we must have __clone last, as there we have the
internal validations, operations and also access to private/protected
members that are not accesible from where clone is being called.Regards,
Alex
I... could not understand that in the slightest. Can you show it in code?
--Larry Garfield
On Wed, May 31, 2023 at 2:53 AM Larry Garfield larry@garfieldtech.com
wrote:
On Tue, May 30, 2023, 19:39 Larry Garfield larry@garfieldtech.com
wrote:To be honest, the current behavior seemed like the natural choice for
me, and I didn't really consider to execute the __clone() method after
the
clone assignments.
Do you have a use-case in mind when you need to forward-pass
information
to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has
more contextual awareness than __clone(), so it should have "first
crack"
at the operation, so that if necessary it can make changes that
__clone()
can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to
"override" or "undo" something that __clone() did. Generally speaking,
if
you have to undo something you just did, you shouldn't have done it in
the
first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it through
as it's kinda hard to reverse later.To me so far also it was natural to assume that __clone is first and only
after that the rest of the operations.
Butwith
operations, be it properties assignment or even a closure,
would
run in the context of the caller of clone and sometimes this might be run
not from a method of the cloned class.An example:
There is a class that represents persons of a fictive country/planet.
Each person has many properties but has also a first name and a last name
and there is a rule: the two names must not start with the same letter.
Both names cannot be changed as they are defined readonly.
Creation of new persons can be done using new for new random properties
or
using clone to preserve existing properties. But in both cases the first
name and last name are randomly chosen.
If we want to control the last name value during clone that would be
possible using thewith
operation but the logic to allocate a first
name
will only happen in__clone()
method.To be able to achieve this we must have __clone last, as there we have
the
internal validations, operations and also access to private/protected
members that are not accesible from where clone is being called.Regards,
AlexI... could not understand that in the slightest. Can you show it in code?
Sorry for that. Here you go: https://3v4l.org/JIBoI/rfc#vgit.master
If __clone would be first, there is no way to enforce the rule that a
person cannot have their first name starting with the same letter as last
name.
Regards,
Alex
On Tue, May 30, 2023, 19:39 Larry Garfield larry@garfieldtech.com
wrote:To be honest, the current behavior seemed like the natural choice
for
me, and I didn't really consider to execute the __clone() method
after
the
clone assignments.
Do you have a use-case in mind when you need to forward-pass
information
to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has
more contextual awareness than __clone(), so it should have "first
crack"
at the operation, so that if necessary it can make changes that
__clone()
can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to
"override" or "undo" something that __clone() did. Generally
speaking,
if
you have to undo something you just did, you shouldn't have done it in
the
first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it
through
as it's kinda hard to reverse later.To me so far also it was natural to assume that __clone is first and
only
after that the rest of the operations.
Butwith
operations, be it properties assignment or even a closure,
would
run in the context of the caller of clone and sometimes this might be
run
not from a method of the cloned class.An example:
There is a class that represents persons of a fictive country/planet.
Each person has many properties but has also a first name and a last
name
and there is a rule: the two names must not start with the same letter.
Both names cannot be changed as they are defined readonly.
Creation of new persons can be done using new for new random properties
or
using clone to preserve existing properties. But in both cases the
first
name and last name are randomly chosen.
If we want to control the last name value during clone that would be
possible using thewith
operation but the logic to allocate a first
name
will only happen in__clone()
method.To be able to achieve this we must have __clone last, as there we have
the
internal validations, operations and also access to private/protected
members that are not accesible from where clone is being called.Regards,
AlexI... could not understand that in the slightest. Can you show it in
code?Sorry for that. Here you go: https://3v4l.org/JIBoI/rfc#vgit.master
If __clone would be first, there is no way to enforce the rule that a
person cannot have their first name starting with the same letter as last
name.
I'm not sure that's what __clone should be used for.
This looks like a perfect use case for property hooks, isn't it?
On my side, I would find it unexpected that __clone is called after because
that would break cloning expectations:
Imagine you have a __clone that does some deep cloning (a much more typical
scenario for this construct),
Let's say __clone() does $this->foo = clone $this->foo;
Imagine now that you do: clone $obj with (foo: $bar)
I'd expect $obj->foo === $bar after this. But if __clone is called after,
that won't be true, and I don't see how that could be "fixed" if we swap
the order. Would you?
Nicolas
On Tue, May 30, 2023, 19:39 Larry Garfield larry@garfieldtech.com
wrote:To be honest, the current behavior seemed like the natural choice
for
me, and I didn't really consider to execute the __clone() method
after
the
clone assignments.
Do you have a use-case in mind when you need to forward-pass
information
to
__clone()?Not a specific one off hand. It's more a conceptual question.
with
has
more contextual awareness than __clone(), so it should have "first
crack"
at the operation, so that if necessary it can make changes that
__clone()
can then respond to. The inverse doesn't make sense.The only reason for
with
to come after would be to allowwith
to
"override" or "undo" something that __clone() did. Generally
speaking,
if
you have to undo something you just did, you shouldn't have done it in
the
first place, so that's a less compelling combination.This one isn't a deal breaker, but we should be sure to think it
through
as it's kinda hard to reverse later.To me so far also it was natural to assume that __clone is first and
only
after that the rest of the operations.
Butwith
operations, be it properties assignment or even a closure,
would
run in the context of the caller of clone and sometimes this might be
run
not from a method of the cloned class.An example:
There is a class that represents persons of a fictive country/planet.
Each person has many properties but has also a first name and a last
name
and there is a rule: the two names must not start with the same letter.
Both names cannot be changed as they are defined readonly.
Creation of new persons can be done using new for new random properties
or
using clone to preserve existing properties. But in both cases the
first
name and last name are randomly chosen.
If we want to control the last name value during clone that would be
possible using thewith
operation but the logic to allocate a first
name
will only happen in__clone()
method.To be able to achieve this we must have __clone last, as there we have
the
internal validations, operations and also access to private/protected
members that are not accesible from where clone is being called.Regards,
AlexI... could not understand that in the slightest. Can you show it in
code?Sorry for that. Here you go: https://3v4l.org/JIBoI/rfc#vgit.master
If __clone would be first, there is no way to enforce the rule that a
person cannot have their first name starting with the same letter as last
name.I'm not sure that's what __clone should be used for.
This looks like a perfect use case for property hooks, isn't it?On my side, I would find it unexpected that __clone is called after because
that would break cloning expectations:
Imagine you have a __clone that does some deep cloning (a much more typical
scenario for this construct),
Let's say __clone() does $this->foo = clone $this->foo;Imagine now that you do: clone $obj with (foo: $bar)
I'd expect $obj->foo === $bar after this. But if __clone is called after,
that won't be true, and I don't see how that could be "fixed" if we swap
the order. Would you?Nicolas
Oh, interesting. There's a nice example case. To make it a bit more concrete, and think aloud...
class Pet {
public function __construct(
public readonly string $name = 'Fluffy',
) {}
}
class Address { ... }
class Person {
public function __construct(
public readonly Pet $pet,
public readonly Address $address,
}
public function __clone() {
// Legal now thanks to a previous RFC.
$this->address = clone ($this->address);
}
}
$p = new Person(new Pet(), new Address(blah));
// The person gets a new pet.
$p2 = clone $p with (pet: new Pet('Bonzo'));
In this case, the order of operations is irrelevant.
// The person moves.
$newAddr = new Address(whatever);
$p3 = clone $p2 with (address: $newAddr);
In this case, if the with
happens first, then the new address object is cloned needlessly, but that probably doesn't hurt anything. But $newAddr !== $p3->address.
If the __clone()
happens first, then the existing address object is cloned needlessly, but that probably doesn't hurt anything. Because the assignment happens second, $newAddr === $p3->address.
So I suppose that's a point in favor of __clone() first.
Another option could be to run with
first, but then pass a list of the keys that were modified (probably not their values, just the keys?) to __clone(). It can then do conditional cloning based on that if appropriate. I... don't know if that's a good idea or not, nor what the BC implications might be.
Máté, your thoughts?
--Larry Garfield
Hi Larry,
In this case, if the with
happens first, then the new address object is
cloned needlessly, but that probably doesn't hurt anything. But $newAddr
!== $p3->address.
Yes, I agree with this: "clone $this with ["x" => "y"];" is the easiest to
mentally model as a shorthand for "$self = clone $this; $self->x = "y";".
If we agree with this model, then it would be weird to execute __clone() at
the end indeed. But I think Nicolas' example explained this fact much
better than I could. :) Also, separating __clone() from the rest of the
clone opcode would be .
And you are right, some objects may be cloned unnecessarily, which doesn't
hurt, but isn't ideal for sure. I should mention that in ideal
circumstances (when all properties are readonly + all of them are
initialized during construction + none of them are mutable internally),
deep cloning is not 100% required, unless only for defensive programming
purposes.
Regards,
Máté
Hi Larry,
In this case, if the
with
happens first, then the new address object iscloned needlessly, but that probably doesn't hurt anything. But $newAddr
!== $p3->address.Yes, I agree with this: "clone $this with ["x" => "y"];" is the easiest to
mentally model as a shorthand for "$self = clone $this; $self->x = "y";".
If we agree with this model, then it would be weird to execute __clone() at
the end indeed. But I think Nicolas' example explained this fact much
better than I could. :) Also, separating __clone() from the rest of the
clone opcode would be .And you are right, some objects may be cloned unnecessarily, which doesn't
hurt, but isn't ideal for sure. I should mention that in ideal
circumstances (when all properties are readonly + all of them are
initialized during construction + none of them are mutable internally),
deep cloning is not 100% required, unless only for defensive programming
purposes.Regards,
Máté
Where all properties are readonly, and if an object those are also readonly, and all are assigned in the constructor...
Yeah, that ideal case is kinda narrow, and likely will remain so for a long while yet.
Whichever order it goes in, that should be documented explicitly and the reasoning for it included. (Feel free to pilfer my examples above extensively if that helps.)
As for the syntax itself, my preferences, in order, would be:
-
clone $foo with (...), where (...) follows named-argument syntax in all its variants and forms, which includes ...$arr.
-
clone $foo with $array, where $array is an honest to goodness assoc array/array literal, created by any means the developer wants.
Either of those are equally expressive; the first is, IMO, cleaner and easier to read/type, and probably nicer on static analysis tools, but they're still both equally expressive.
Anything less than that is, IMO, creating unnecessary confusion about how the syntax behaves that will trip up developers left and right.
--Larry Garfield
Hi Larry,
In this case, if the
with
happens first, then the new address object iscloned needlessly, but that probably doesn't hurt anything. But $newAddr
!== $p3->address.Yes, I agree with this: "clone $this with ["x" => "y"];" is the easiest to
mentally model as a shorthand for "$self = clone $this; $self->x = "y";".
If we agree with this model, then it would be weird to execute __clone() at
the end indeed. But I think Nicolas' example explained this fact much
better than I could. :) Also, separating __clone() from the rest of the
clone opcode would be .And you are right, some objects may be cloned unnecessarily, which doesn't
hurt, but isn't ideal for sure. I should mention that in ideal
circumstances (when all properties are readonly + all of them are
initialized during construction + none of them are mutable internally),
deep cloning is not 100% required, unless only for defensive programming
purposes.Regards,
MátéWhere all properties are readonly, and if an object those are also readonly, and all are assigned in the constructor...
Yeah, that ideal case is kinda narrow, and likely will remain so for a long while yet.
Whichever order it goes in, that should be documented explicitly and the reasoning for it included. (Feel free to pilfer my examples above extensively if that helps.)
As for the syntax itself, my preferences, in order, would be:
clone $foo with (...), where (...) follows named-argument syntax in all its variants and forms, which includes ...$arr.
clone $foo with $array, where $array is an honest to goodness assoc array/array literal, created by any means the developer wants.
Either of those are equally expressive; the first is, IMO, cleaner and easier to read/type, and probably nicer on static analysis tools, but they're still both equally expressive.
Anything less than that is, IMO, creating unnecessary confusion about how the syntax behaves that will trip up developers left and right.
--Larry Garfield
Hello list,
First, I am very much in favor of the named-argument syntax clone $obj with (key: $value, ...)
, with support for argument unpacking.
The syntax is well suited for the most common case, which is regular
wither methods for one or more known properties.
I don't like the array syntax as a default. It has inferior DX for
regular withers, and possible performance impact from the temporary
array.
For the execution scope ideas:
I kind of like the idea to have a special object state or scope effect
where some properties are writable that otherwise wouldn't be.
However, I don't like this kind of concept being introduced as a
special case only for this clone execution scope.
E.g. the current "readonly" implementations actually means write-once
- write-private, applied on property level. But in the clone scope it
would mean something different.
I have one question though:
What could be the best way to modify readonly properties from a parent class?
Obviously we can call a parent wither. But this means we now have two
clone operations, which can have a performance impact.
class C extends B {
private readonly $isGreyscale;
function withColor($color): static {
// One clone operation in B, another in C.
return = clone parent::withColor($color) with (
// Ok it's a bit silly to make this a separate property.
isGreyscale: $color->isGreyscale(),
);
}
}
The same problem also occurs if the parent readonly property is public.
We must call the parent wither, otherwise we have no permission to
update the property.
One workaround is to redeclare the property in the child class.
https://3v4l.org/Rn1CR
I think the solutions/workarounds in both cases are acceptable,
assuming that the parent wither call is rare enough.
There could even be a future compiler optimization where "clone foo()"
will immediately garbage-collect the temporary object, and re-use the
memory space for the clone. I don't know enough about php internals to
know if that would work.
--- Andreas
--
To unsubscribe, visit: https://www.php.net/unsub.php
Where all properties are readonly, and if an object those are also readonly, and all are assigned in the constructor...
Yeah, that ideal case is kinda narrow, and likely will remain so for a long while yet.
Whichever order it goes in, that should be documented explicitly and the reasoning for it included. (Feel free to pilfer my examples above extensively if that helps.)
As for the syntax itself, my preferences, in order, would be:
clone $foo with (...), where (...) follows named-argument syntax in all its variants and forms, which includes ...$arr.
clone $foo with $array, where $array is an honest to goodness assoc array/array literal, created by any means the developer wants.
Either of those are equally expressive; the first is, IMO, cleaner and easier to read/type, and probably nicer on static analysis tools, but they're still both equally expressive.
Anything less than that is, IMO, creating unnecessary confusion about how the syntax behaves that will trip up developers left and right.
--Larry Garfield
Hello list,
First, I am very much in favor of the named-argument syntaxclone $obj with (key: $value, ...)
, with support for argument unpacking.The syntax is well suited for the most common case, which is regular
wither methods for one or more known properties.
I don't like the array syntax as a default. It has inferior DX for
regular withers, and possible performance impact from the temporary
array.
I agree.
For the execution scope ideas:
I kind of like the idea to have a special object state or scope effect
where some properties are writable that otherwise wouldn't be.
However, I don't like this kind of concept being introduced as a
special case only for this clone execution scope.
E.g. the current "readonly" implementations actually means write-once
- write-private, applied on property level. But in the clone scope it
would mean something different.
Yet again, readonly was badly designed and it's biting us. :-( This is one of the reasons Ilija and I are planning to take another swing at asymmetric visibility, which solves nearly all of these issues. Or, it will if we can avoid bikeshedding over the syntax again...
--Larry Garfield
Hi
The syntax is well suited for the most common case, which is regular
wither methods for one or more known properties.
I don't like the array syntax as a default. It has inferior DX for
regular withers, and possible performance impact from the temporary
array.
Again [1]: The choice of syntax cannot influence runtime performance
after the code is compiled, because it is just that - syntax.
It would be perfectly possible to specifically handle the case where the
'with' is followed by an array literal and then not construct a
temporary array. In fact this is how the current implementation works:
It doesn't actually support arrays yet, it just borrows the syntax of an
array, because it more naturally extends to supporting arrays in the future.
Best regards
Tim Düsterhus
[1] See also my previous email: https://externals.io/message/120048#120073
Hello everyone,
On Mon, May 29, 2023 at 6:48 AM Michał Marcin Brzuchalski
michal.brzuchalski@gmail.com wrote:
So there would be no option to vote on shorthand properties, right?
Array syntax with all properties as strings in quotes probably means no
support from IDE.
I think this is a really bad move, I'd be against it.
I agree. I think going to a single syntax was a good move, but using the literal
property name would have been better imo.
I really think the syntax would benefit from being more similar to
function calls,
with a syntax like:
clone $obj with (prop1: "value");
This brings the familiarity from named arguments, which in this case would
represent the object's properties.
Then for dynamic property name support, you can borrow array unpacking
into an argument list from function calls as well, allowing the dynamic
properties to be set like:
$myProp = 'prop1';
clone $obj with (...[$myProp => 'value']);
This also gives the ability to have create dynamic properties via anonymous
function as well, like so:
$myFunc = function ($original): array {
$props = [];
// add results
return $props;
};
clone $obj with (...$myFunc($obj));
You even have the ability to pass it whatever you want including the original
object to operate off of.
Regards,
Brandon Jackson
Hi
So there would be no option to vote on shorthand properties, right?
Array syntax with all properties as strings in quotes probably means no
support from IDE.
I think this is a really bad move, I'd be against it.
I expect any IDE or SA tool worth its salt to be able to understand that
a string-literal is … a string-literal and treat it as constant. Whether
there are quotes around that literal or not should not make a meaningful
difference for automated processing.
I'm fine with the proposed syntax [1], but agree that "clone with
expressions" would be useful to have even in the initial version.
Best regards
Tim Düsterhus
[1] It's consistent with existing syntax and that is important.
Hi Everyone,
In the meanwhile, I changed my proposal to use [] instead of {} after the
"with" clause due to its better receptance.
In "Property name expressions" section's first code example I think you
intended to do clone $self
(not clone $this
) inside the foreach() loop.
Also, I have a feeling that it would be better to implement clone $object with $properties
syntax first and if that is accepted think
about expressions support. It's not a strong feeling though.
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
In "Property name expressions" section's first code example I think you
intended to doclone $self
(notclone $this
) inside the foreach() loop.Also, I have a feeling that it would be better to implement
clone $object with $properties
syntax first and if that is accepted think
about expressions support. It's not a strong feeling though.
Actually, with some more thought, my opinion now is that introduction of
property name expressions is not justified. And will be a reason for me
to vote No.
I'm still not decided about which syntax I'd prefer, maybe you should do
a poll with these basic options. Sorry, if there was one and I missed it.
-
clone $object with $properties;
-
clone($object, $properties);
-
clone($object, prop1: $var1, prop2: $var2);
-
clone($object, function ($clone) use ($properties) {
foreach ($properties as $name => $value) {
$clone->{$name} = $value;
}
}); -
clone $obj with (prop1: $var1, prop2: $var2);
note: $properties is iterable
note: in 2-4) brackets are potentially optional.
note: imho, 5) is similar to 3), but worse.
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_with
| So far, all “clone with” examples introduced in the current RFC used
| fixed property names which were referenced as identifiers followed by
| a colon (:), while the values to be assigned were expressions.
|
| …
|
| both side of the assignment is an expression, separated by =>
I don't think there should be two syntaxes here. Either allow
expressions on the left side of the ':' or always use '=>' and require a string on
the LHS. The example already allows for using a string on the LHS in
{"foo" => 1}, so I think that the most useful choice. I don't think in
{PROPERTY_NAME => 1} , the LHS should be interpreted as the literal
value PROPERTY_NAME, but only the value of that constant.
cheers,
Derick
Hi Everyone,
Quite some time after mentioning the "clone with" construct the first time
(at the end of the
https://wiki.php.net/rfc/write_once_properties#run-time_behaviour section),
finally I managed to create a working implementation for this feature which
would make it possible to properly modify readonly properties
while simplifying how we write "wither" methods:
https://wiki.php.net/rfc/clone_withRegards,
Máté Kocsis
I apologize if this has been discussed before, as I have fallen very
behind on internals discussions. I think it would be helpful to add an
example where the object being cloned accesses its properties.
Something like this:
$b = clone $a with ["count" => $a->count * 2];
If this is not valid, or if it has unexpected behavior or semantics,
that should also be included in the RFC.
(Resending my earlier message to Levi:)
Hi Levi,
I apologize if this has been discussed before, as I have fallen very
behind on internals discussions. I think it would be helpful to add an
example where the object being cloned accesses its properties.
Something like this:$b = clone $a with ["count" => $a->count * 2];
If this is not valid, or if it has unexpected behavior or semantics,
that should also be included in the RFC.
It hasn't been discussed yet, but thankfully, there's nothing special about
it:
the LHS is evaluated first, then comes the RHS. Nevertheless, I've just
clarified this in the RFC,
and thanks for pointing this out!
Regards,
Máté