Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_properties
In short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.
Cheers,
Máté Kocsis
Hey Mark,
Asking here as well, so that others are aware: is
unset($this->aPropertyThatWasNotInitializedBefore)
still working with
this patch?
Also, are the semantics around __get
, __set
and __isset
the same as
with typed properties?
In the RFC, you have unset($foo->c)
as disallowed: the property isn't
declared nor initialized though. I'd expect this to fail only after first
initialization?
Marco Pivetta
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté Kocsis
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté Kocsis
As envisoned, does this allow for a property to be set to a dynamic value? My concern is that while a public locked/writeonce property is great for access, it doesn't do anything to enable lazy setting on first access. In fact the only way to do that would be to make it private and wrap access in a method, which would look exactly like that does now but with an extra keyword that doesn't actually offer much.
You could set the value in advance in the constructor, but then it's not lazy, just locked.
Is there a way it could support lazy-on-first-use then locked?
--Larry Garfield
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté KocsisAs envisoned, does this allow for a property to be set to a dynamic
value? My concern is that while a public locked/writeonce property is
great for access, it doesn't do anything to enable lazy setting on first
access. In fact the only way to do that would be to make it private and
wrap access in a method, which would look exactly like that does now but
with an extra keyword that doesn't actually offer much.You could set the value in advance in the constructor, but then it's not
lazy, just locked.Is there a way it could support lazy-on-first-use then locked?
Máté did run a few teats: operating on a lazy value (before initialisation)
seems to work as expected ?
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté KocsisAs envisoned, does this allow for a property to be set to a dynamic
value? My concern is that while a public locked/writeonce property is
great for access, it doesn't do anything to enable lazy setting on first
access. In fact the only way to do that would be to make it private and
wrap access in a method, which would look exactly like that does now but
with an extra keyword that doesn't actually offer much.You could set the value in advance in the constructor, but then it's not
lazy, just locked.Is there a way it could support lazy-on-first-use then locked?
Máté did run a few teats: operating on a lazy value (before initialisation)
seems to work as expected ?
Erm. I don't think I was clear in my intent. I would expect that operating on one of these properties before it's initialized will throw an error:
class Foo {
readonly public string $bar;
}
$f = new Foo();
print $f->bar; // This should fail.
My question is whether this will work:
class Foo {
readonly public string $bar = $this->value();
private function value(): string { return "hello"; }
}
$f = new Foo();
print $f->bar; // I would want this print "hello".
Does that work currently or no? If so, this is pretty sweet. If not, it seems to be of limited use.
--Larry Garfield
I would expect that operating on one of these properties before it's
initialized will throw an error:
Actually, the RFC only says that the "immutability" of properties is
guaranteed after initialization.
We could of course change this premise, but that would make some important
use-cases (like
what Marco's ProxyManager has) impossible to do. Furthermore, unsetting
properties could be
completely disabled by another RFC, given there is a migration path for the
legitimiate use-cases.
As far as I remember, the "Locked Classes" (
https://wiki.php.net/rfc/locked-classes) and lately the
"Rigid Properties" (https://github.com/php/php-src/pull/5170) RFCs try to
have their shots at it.
Does that work currently or no? If so, this is pretty sweet. If not, it
seems to be of limited use.
No, it doesn't work, and it seems to be an unrelated feature for me. As far
as I understand your
example, it's the topic of the "Constant expressions" RFC.
I believe the behaviour proposed by my RFC would be still useful in many
cases where one wants
to be sure that no unexpected modifications can happen with a property. My
use-cases
would mainly include objects storing different kind of data: events, value
objects, data transfer objects.
Máté Kocsis
Someone recently requested something similar for a PHP static analysis tool
I wrote (https://psalm.dev/r/f75997a263), though that version only allows
lazy initialisation inside the class in which a given property is declared.
Personally I don't like either approach – I think per-property getters and
setters would be a more appropriate venue for this functionality, something
like:
property int $s {
public get;
private set;
}
This pattern could also be extended to support per-property getter and
setter methods.
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté Kocsis
On Thu, Feb 20, 2020 at 1:27 AM Matthew Brown matthewmatthew@gmail.com
wrote:
Someone recently requested something similar for a PHP static analysis tool
I wrote (https://psalm.dev/r/f75997a263), though that version only allows
lazy initialisation inside the class in which a given property is declared.Personally I don't like either approach – I think per-property getters and
setters would be a more appropriate venue for this functionality, something
like:property int $s {
public get;
private set;
}This pattern could also be extended to support per-property getter and
setter methods.
While I certainly like the idea of per-property getters/setters, I think
that both of these have their place. This RFC proposes readonly properties,
that can be initialized once, and then never modified again, even within
the same class. Getters/setters only provide the possibility of having
asymmetric visibility: They prevent modifying the property from outside the
class, but it can still be modified inside the class. In that, readonly
properties offer a stronger guarantee.
Of course, that does leave the question of how often you need one or the
other. Maybe just the asymmetric visibility is sufficient for most
practical purposes, in which case it may not be worthwhile to introduce
readonly properties as a separate feature.
Two comments on the specifics of the RFC:
The RFC allows specifying a default value for readonly properties. However,
a property for which a default value has been specified will always have
that value, as it cannot be overwritten in the constructor. If you write
"public readonly int $foo = 42", then $object->foo is always going to be
- I'm not sure what that would ever be useful for, and it seems like
something that is bound to be confusing. Maybe it would make more sense to
forbid readonly properties with default values? (That way, the rule that
readonly properties have to be typed falls out naturally as well -- untyped
properties always have a default.)
Regarding the keyword choice, I think you can drop "sealed" from the list,
as it is an established term that affects inheritance, not mutability. Of
the choices you present, "immutable", "readonly" and "writeonce" seem like
the most viable candidates. "writeonce", while the one that is most
technically accurate, is also unnecessarily technically accurate and not
intuitive. From the perspective of an API consumer, I think that "readonly"
is the most accurate description of how they are supposed to interact with
the property. The API contract you want to expose is that they can only
read from the property, not write to it. Calling it "writeonce" would be
quite confusing in that context, because the API consumer is never expected
to write to the property. In the majority of cases you will be providing
fully initialized objects, in which case they are indeed readonly for the
consumer -- the details of the write-once property are only relevant in
special cases like ReflectionClass::newObjectWithoutConstructor() for
serialization libraries, or lazy initialization like in Marco's
ProxyManager.
Regards,
Nikita
Hi Internals,
I'd like to move my RFC forward to the discussion phase:
https://wiki.php.net/rfc/write_once_propertiesIn short, I propose to add support for a new property modifier that would
allow properties to be initialized, but not modified afterwards.Cheers,
Máté Kocsis
On Thu, Feb 20, 2020 at 1:27 AM Matthew Brown matthewmatthew@gmail.com
wrote:Someone recently requested something similar for a PHP static analysis tool
I wrote (https://psalm.dev/r/f75997a263), though that version only allows
lazy initialisation inside the class in which a given property is declared.Personally I don't like either approach – I think per-property getters and
setters would be a more appropriate venue for this functionality, something
like:property int $s {
public get;
private set;
}This pattern could also be extended to support per-property getter and
setter methods.While I certainly like the idea of per-property getters/setters, I think
that both of these have their place. This RFC proposes readonly properties,
that can be initialized once, and then never modified again, even within
the same class. Getters/setters only provide the possibility of having
asymmetric visibility: They prevent modifying the property from outside the
class, but it can still be modified inside the class. In that, readonly
properties offer a stronger guarantee.Of course, that does leave the question of how often you need one or the
other. Maybe just the asymmetric visibility is sufficient for most
practical purposes, in which case it may not be worthwhile to introduce
readonly properties as a separate feature.Two comments on the specifics of the RFC:
The RFC allows specifying a default value for readonly properties. However,
a property for which a default value has been specified will always have
that value, as it cannot be overwritten in the constructor. If you write
"public readonly int $foo = 42", then $object->foo is always going to be
- I'm not sure what that would ever be useful for, and it seems like
something that is bound to be confusing. Maybe it would make more sense to
forbid readonly properties with default values? (That way, the rule that
readonly properties have to be typed falls out naturally as well -- untyped
properties always have a default.)Regarding the keyword choice, I think you can drop "sealed" from the list,
as it is an established term that affects inheritance, not mutability. Of
the choices you present, "immutable", "readonly" and "writeonce" seem like
the most viable candidates. "writeonce", while the one that is most
technically accurate, is also unnecessarily technically accurate and not
intuitive. From the perspective of an API consumer, I think that "readonly"
is the most accurate description of how they are supposed to interact with
the property. The API contract you want to expose is that they can only
read from the property, not write to it. Calling it "writeonce" would be
quite confusing in that context, because the API consumer is never expected
to write to the property. In the majority of cases you will be providing
fully initialized objects, in which case they are indeed readonly for the
consumer -- the details of the write-once property are only relevant in
special cases like ReflectionClass::newObjectWithoutConstructor() for
serialization libraries, or lazy initialization like in Marco's
ProxyManager.Regards,
Nikita
From Máté Kocsis kocsismate90@gmail.com wrote:
No, it doesn't work, and it seems to be an unrelated feature for me. As far
as I understand your
example, it's the topic of the "Constant expressions" RFC.I believe the behaviour proposed by my RFC would be still useful in many
cases where one wants
to be sure that no unexpected modifications can happen with a property. My
use-cases
would mainly include objects storing different kind of data: events, value
objects, data transfer objects.
Yeah, I'm definitely thinking in relation to the earlier discussion, since I think they're all inter-related. (This, property accessors, and constant expressions.)
As Nikita notes above, a read-only property with a default value is... basically a constant already. So that's not really useful.
For defined-later readonly properties, I'm not sure how the earlier point about reading an unintialized property isn't valid. Currently:
class Foo {
public string $bar;
}
$f = new Foo();
print $f->bar; // this throws a TypeError.
I would expect the exact same behavior if $bar were marked readonly/locked/whatever. Are you saying that's not the case?
I do think it's fair to bring in the property accessor discussion here, as property accessors would allow for the same net functionality as this property as a special case, albeit with likely more syntax. AFAIR the issue with them before wasn't that people opposed the idea, just the performance impact.
If we could address the performance impact, that would give us much more functionality-for-the-buck, including an equivalent of read-only properties including potentially lazy initialization. Or derive-on-demand behavior would also be a big increase in functionality.
It's not that I don't see a value to this RFC; I actually have a few places in my own code where I could use it. It's that I see it as being of fairly narrow use, so I'm trying to figure out how to increase it so that the net-win is worth it.
--Larry Garfield
Yeah, I'm definitely thinking in relation to the earlier discussion, since
I think they're all inter-related. (This, property accessors, and constant
expressions.)
The biggest question is whether it's worth to support both readonly
properties and property accessors. My answer is clear yes, because there
are many-many
ways to mess with private or protected properties without and public
setters from the outside - in which case property accessors couldn't help
much. I collected some examples I know of:
https://3v4l.org/Ta4PM
Please note that the first two examples also apply to private properties,
while the last one only applies to protected ones.
As Nikita notes above, a read-only property with a default value is...
basically a constant already. So that's not really useful.
I agree that they are not very useful, however I wouldn't restrict their
usage. Mainly because there are probably some legitimate use-cases, but I
also think it would
be advantageous to be consistent with the other languages in this case.
For defined-later readonly properties, I'm not sure how the earlier point
about reading an unintialized property isn't valid. Currently:class Foo {
public string $bar;
}$f = new Foo();
print $f->bar; // this throws a TypeError.I would expect the exact same behavior if $bar were marked
readonly/locked/whatever. Are you saying that's not the case?
Sorry if I didn't exactly get the question/example, but what I can tell you
is that currently an Error exception is thrown with the
"Typed property Foo::$bar must not be accessed before initialization"
message, and it would be the case with my patch as well
since it doesn't affect the reading side.
The situation is the same when it comes to unsetting uninitialized typed
properties. Currently, these properties can be unset with no problem (and
the
__get(), __set() etc. magic methods are then invoked when accessing them),
and the same would happen with my patch.
If we could address the performance impact, that would give us much more
functionality-for-the-buck, including an equivalent of read-only properties
including potentially lazy initialization. Or derive-on-demand behavior
would also be a big increase in functionality.It's not that I don't see a value to this RFC; I actually have a few
places in my own code where I could use it. It's that I see it as being of
fairly narrow use, so I'm trying to figure out how to increase it so that
the net-win is worth it.
The reason why I brought up this RFC is that I'd really like to add
first-class support for immutable objects, and it seemed to be a good idea
to first go for readonly properties.
This way, the scope of an immutable object RFC gets smaller, while it's
possible to only have readonly properties alone.
Regards,
Máté
Yeah, I'm definitely thinking in relation to the earlier discussion, since
I think they're all inter-related. (This, property accessors, and constant
expressions.)The biggest question is whether it's worth to support both readonly
properties and property accessors. My answer is clear yes, because there
are many-many
ways to mess with private or protected properties without and public
setters from the outside - in which case property accessors couldn't help
much. I collected some examples I know of:
https://3v4l.org/Ta4PM
I didn't even know you could do some of those. That's horrifying. :-)
As Nikita notes above, a read-only property with a default value is...
basically a constant already. So that's not really useful.I agree that they are not very useful, however I wouldn't restrict their
usage. Mainly because there are probably some legitimate use-cases, but I
also think it would
be advantageous to be consistent with the other languages in this case.
If they could do something that class constants can't, that would make them useful. If not, then I feel like it would just be introducing new syntax for the same thing, without much benefit. (I'm thinking of, eg, could you set them by default to a new Foo() object, which you could then modify the Foo but not change it for another object, thus moving that initialization out of the constructor? That sort of thing.)
If we could address the performance impact, that would give us much more
functionality-for-the-buck, including an equivalent of read-only properties
including potentially lazy initialization. Or derive-on-demand behavior
would also be a big increase in functionality.It's not that I don't see a value to this RFC; I actually have a few
places in my own code where I could use it. It's that I see it as being of
fairly narrow use, so I'm trying to figure out how to increase it so that
the net-win is worth it.The reason why I brought up this RFC is that I'd really like to add
first-class support for immutable objects, and it seemed to be a good idea
to first go for readonly properties.
This way, the scope of an immutable object RFC gets smaller, while it's
possible to only have readonly properties alone.Regards,
Máté
I'm totally on board for better value object support, so that's a good motive for me. The question I have is whether this is really a good stepping stone in that direction or if it would lead down a wrong path and lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's think that through down that path. How would write-once properties lead into properly immutable value objects? Or do they give us that themselves?
The biggest challenge for immutable objects, IMO, is evolving them. Eg, $result->withContentType(...) to use the PSR-7 example. Would we expect people to do it with a method like that, or would there be some other mechanism? If the properties are public, would we offer a more syntactic way to modify them directly?
The with*() method style requires cloning the object. What happens to the locked status of a set property if the object is cloned? Are they then settable again, or do they come pre-locked?
Neither of those seem good, now that I think about it. If they come pre-locked, then you really can't clone, change one property, and return the new one (as is the standard practice now in that case). If they don't come pre-locked, then the newly created object can have everything on it changed, once, which creates a loophole. I'm not sure what the right answer is here.
My other concern is a public property (the most likely use case) would have to be set in the constructor. If it's not, then callers cannot rely on it having been set yet if it's set lazily. And if code inside the class tries to set it lazily, it may already have been set by some external code (rightly or wrongly) and cause a failure.
How do we address that? There's absolutely use cases where setting everything in the constructor ahead of time is what you'd do anyway, but there are plenty where you wouldn't want to, either, which creates a race condition for who sets it first, or tries to access it before it gets set, etc. (This is where my repeated questions about lazy initialization come from.)
--Larry Garfield
When writing immutable classes, I want to be able to set properties in
static factories and in wither methods.
Once the new instance is sent to the outside world, its properties can be
locked to prevent further modification.
This sounds to me like we need different modes. Either the object itself
would have different states over time, or the object stays the same and
instead some methods have mutation permission on newly created objects.
This could be seen as a runtime state problem or as a compile time code
verification problem.
Yeah, I'm definitely thinking in relation to the earlier discussion,
since
I think they're all inter-related. (This, property accessors, and
constant
expressions.)The biggest question is whether it's worth to support both readonly
properties and property accessors. My answer is clear yes, because there
are many-many
ways to mess with private or protected properties without and public
setters from the outside - in which case property accessors couldn't help
much. I collected some examples I know of:
https://3v4l.org/Ta4PMI didn't even know you could do some of those. That's horrifying. :-)
As Nikita notes above, a read-only property with a default value is...
basically a constant already. So that's not really useful.I agree that they are not very useful, however I wouldn't restrict their
usage. Mainly because there are probably some legitimate use-cases, but I
also think it would
be advantageous to be consistent with the other languages in this case.If they could do something that class constants can't, that would make
them useful. If not, then I feel like it would just be introducing new
syntax for the same thing, without much benefit. (I'm thinking of, eg,
could you set them by default to a new Foo() object, which you could then
modify the Foo but not change it for another object, thus moving that
initialization out of the constructor? That sort of thing.)If we could address the performance impact, that would give us much
more
functionality-for-the-buck, including an equivalent of read-only
properties
including potentially lazy initialization. Or derive-on-demand
behavior
would also be a big increase in functionality.It's not that I don't see a value to this RFC; I actually have a few
places in my own code where I could use it. It's that I see it as
being of
fairly narrow use, so I'm trying to figure out how to increase it so
that
the net-win is worth it.The reason why I brought up this RFC is that I'd really like to add
first-class support for immutable objects, and it seemed to be a good
idea
to first go for readonly properties.
This way, the scope of an immutable object RFC gets smaller, while it's
possible to only have readonly properties alone.Regards,
MátéI'm totally on board for better value object support, so that's a good
motive for me. The question I have is whether this is really a good
stepping stone in that direction or if it would lead down a wrong path and
lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's
think that through down that path. How would write-once properties lead
into properly immutable value objects? Or do they give us that themselves?The biggest challenge for immutable objects, IMO, is evolving them. Eg,
$result->withContentType(...) to use the PSR-7 example. Would we expect
people to do it with a method like that, or would there be some other
mechanism? If the properties are public, would we offer a more syntactic
way to modify them directly?The with*() method style requires cloning the object. What happens to the
locked status of a set property if the object is cloned? Are they then
settable again, or do they come pre-locked?Neither of those seem good, now that I think about it. If they come
pre-locked, then you really can't clone, change one property, and return
the new one (as is the standard practice now in that case). If they don't
come pre-locked, then the newly created object can have everything on it
changed, once, which creates a loophole. I'm not sure what the right
answer is here.My other concern is a public property (the most likely use case) would
have to be set in the constructor. If it's not, then callers cannot rely
on it having been set yet if it's set lazily. And if code inside the class
tries to set it lazily, it may already have been set by some external code
(rightly or wrongly) and cause a failure.How do we address that? There's absolutely use cases where setting
everything in the constructor ahead of time is what you'd do anyway, but
there are plenty where you wouldn't want to, either, which creates a race
condition for who sets it first, or tries to access it before it gets set,
etc. (This is where my repeated questions about lazy initialization come
from.)--Larry Garfield
I'm totally on board for better value object support, so that's a good motive for me. The question I have is whether this is really a good stepping stone in that direction or if it would lead down a wrong path and lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's think that through down that path. How would write-once properties lead into properly immutable value objects? Or do they give us that themselves?
The biggest challenge for immutable objects, IMO, is evolving them. Eg, $result->withContentType(...) to use the PSR-7 example. Would we expect people to do it with a method like that, or would there be some other mechanism? If the properties are public, would we offer a more syntactic way to modify them directly?
The with*() method style requires cloning the object. What happens to the locked status of a set property if the object is cloned? Are they then settable again, or do they come pre-locked?
Neither of those seem good, now that I think about it. If they come pre-locked, then you really can't clone, change one property, and return the new one (as is the standard practice now in that case). If they don't come pre-locked, then the newly created object can have everything on it changed, once, which creates a loophole. I'm not sure what the right answer is here.
My other concern is a public property (the most likely use case) would have to be set in the constructor. If it's not, then callers cannot rely on it having been set yet if it's set lazily. And if code inside the class tries to set it lazily, it may already have been set by some external code (rightly or wrongly) and cause a failure.
How do we address that? There's absolutely use cases where setting everything in the constructor ahead of time is what you'd do anyway, but there are plenty where you wouldn't want to, either, which creates a race condition for who sets it first, or tries to access it before it gets set, etc. (This is where my repeated questions about lazy initialization come from.)
I have struggled to follow this RFC thread fully, so if I am getting something out of context, please note that and I apologize in advance.
However, it would see that rules for write once
properties to support lazy loading would be rather simple:
- Write-once properties can only be updated once.
- Write-once properties can only be updated within the class where they are declared.
- If you want to update a property from outside the class, create a set_<property>() method to allow it to happen.
- If you do not want it to be set externally, do not implement a set_<property>() method.
- If you want it to be implemented externally sometimes but not others, implement guard classes inside the set_<property>() method.
I think that addresses all scenarios, no?
-Mike
I'm totally on board for better value object support, so that's a good motive for me. The question I have is whether this is really a good stepping stone in that direction or if it would lead down a wrong path and lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's think that through down that path. How would write-once properties lead into properly immutable value objects? Or do they give us that themselves?
The biggest challenge for immutable objects, IMO, is evolving them. Eg, $result->withContentType(...) to use the PSR-7 example. Would we expect people to do it with a method like that, or would there be some other mechanism? If the properties are public, would we offer a more syntactic way to modify them directly?
The with*() method style requires cloning the object. What happens to the locked status of a set property if the object is cloned? Are they then settable again, or do they come pre-locked?
Neither of those seem good, now that I think about it. If they come pre-locked, then you really can't clone, change one property, and return the new one (as is the standard practice now in that case). If they don't come pre-locked, then the newly created object can have everything on it changed, once, which creates a loophole. I'm not sure what the right answer is here.
My other concern is a public property (the most likely use case) would have to be set in the constructor. If it's not, then callers cannot rely on it having been set yet if it's set lazily. And if code inside the class tries to set it lazily, it may already have been set by some external code (rightly or wrongly) and cause a failure.
How do we address that? There's absolutely use cases where setting everything in the constructor ahead of time is what you'd do anyway, but there are plenty where you wouldn't want to, either, which creates a race condition for who sets it first, or tries to access it before it gets set, etc. (This is where my repeated questions about lazy initialization come from.)
I have struggled to follow this RFC thread fully, so if I am getting
something out of context, please note that and I apologize in advance.However, it would see that rules for
write once
properties to support
lazy loading would be rather simple:
- Write-once properties can only be updated once.
- Write-once properties can only be updated within the class where
they are declared.
This is the common use case I think many envision, but nothing in the proposal requires that. A public write-once property (as currently written) would be world-readable, and world-writeable, once.
Separate visibility for internal and external access is a separate matter. (Also potentially useful, but not part of the write-once proposal at the moment.)
- If you want to update a property from outside the class, create a
set_<property>() method to allow it to happen.- If you do not want it to be set externally, do not implement a
set_<property>() method.- If you want it to be implemented externally sometimes but not
others, implement guard classes inside the set_<property>() method.I think that addresses all scenarios, no?
-Mike
It does not.
-
Race condition if I assume that a public write-once property is a materialized value, but access it before it gets materialized.
-
Race condition if internal non-constructor code wants to set the value, but some external routine has set it first.
-
Cloning creates an interesting and complicated case of both of the above. Does a cloned object start with its write-once bits reset or no? There's problems both ways.
Making a write-once property implicitly write-only-from-inside-the-class would help address point 2, but not points 1 or 3.
Adding separate get/set visibility modifiers is another interesting idea, but is separate and should be evaluated on its own merits. It is not at this time part of this proposal.
--Larry Garfield
Hey Larry,
It does not.
- Race condition if I assume that a public write-once property is a
materialized value, but access it before it gets materialized.
This is alrey true for any public typed properties without default values,
and why static analysis tools pick up missing initialisation since ages.
- Race condition if internal non-constructor code wants to set the value,
but some external routine has set it first.
Internal ctors are part of the category above: tools like psalm pick this
up as well.
- Cloning creates an interesting and complicated case of both of the
above. Does a cloned object start with its write-once bits reset or no?
There's problems both ways.
Not a problem: we seem to be making a problem out of the pattern used in
current PSR-7 implementations, which is following:
final class Foo
{
public function withBar($bar):self {
$instance = clone $this;
$instance->foo = $foo;
return $instance;
}
}
The solution is trivial: don't use cloning:
final class Foo
{
public function withBar($bar):self {
$instance = new self();
$instance->foo = $foo;
// more assignments here - unavoidable
return $instance;
}
}
The solution is trivial: don't use cloning:
final class Foo
{
public function withBar($bar):self {
$instance = new self();
$instance->foo = $foo;
// more assignments here - unavoidable
return $instance;
}
}
This works fine on paper, but is completely impractical as the class grows. Consider a class with 10 such properties, each with their own with* method: switching from clone to explicit assignments means adding 90 lines of code, all of it copy-and-pasted boilerplate that's hard to spot mistakes in.
It's also impossible to use with inheritance, or to compose with traits (as Diactoros does, for instance), because every with* method needs to know the full details of how to create a partial clone.
Regards,
--
Rowan Tommins
[IMSoP]
On 24 February 2020 03:26:19 GMT+00:00, Marco Pivetta ocramius@gmail.com
wrote:The solution is trivial: don't use cloning:
final class Foo
{
public function withBar($bar):self {
$instance = new self();
$instance->foo = $foo;
// more assignments here - unavoidable
return $instance;
}
}This works fine on paper, but is completely impractical as the class
grows. Consider a class with 10 such properties
Yes, considered it: it's fine.
Also, the best approach would be to have a ctor with those 10 properties.
It's also impossible to use with inheritance, or to compose with traits (as
Diactoros does, for instance)
Inheritance would be ditched in favour of even more composition, but even
then, inheritance is not a massive issue, especially when it comes to value
types.
Probably a couple hours of work to make a full PSR-7 implementation with
the old tests running, and the new approach applied ?
For things that are more sensible, like a UUID, a Domain Event or a
Command, this is really a game changer anyway.
The good old Object Calisthenics rules do really apply.
I'm totally on board for better value object support, so that's a good motive for me. The question I have is whether this is really a good stepping stone in that direction or if it would lead down a wrong path and lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's think that through down that path. How would write-once properties lead into properly immutable value objects? Or do they give us that themselves?
The biggest challenge for immutable objects, IMO, is evolving them. Eg, $result->withContentType(...) to use the PSR-7 example. Would we expect people to do it with a method like that, or would there be some other mechanism? If the properties are public, would we offer a more syntactic way to modify them directly?
The with*() method style requires cloning the object. What happens to the locked status of a set property if the object is cloned? Are they then settable again, or do they come pre-locked?
Neither of those seem good, now that I think about it. If they come pre-locked, then you really can't clone, change one property, and return the new one (as is the standard practice now in that case). If they don't come pre-locked, then the newly created object can have everything on it changed, once, which creates a loophole. I'm not sure what the right answer is here.
My other concern is a public property (the most likely use case) would have to be set in the constructor. If it's not, then callers cannot rely on it having been set yet if it's set lazily. And if code inside the class tries to set it lazily, it may already have been set by some external code (rightly or wrongly) and cause a failure.
How do we address that? There's absolutely use cases where setting everything in the constructor ahead of time is what you'd do anyway, but there are plenty where you wouldn't want to, either, which creates a race condition for who sets it first, or tries to access it before it gets set, etc. (This is where my repeated questions about lazy initialization come from.)
I have struggled to follow this RFC thread fully, so if I am getting
something out of context, please note that and I apologize in advance.However, it would see that rules for
write once
properties to support
lazy loading would be rather simple:
- Write-once properties can only be updated once.
- Write-once properties can only be updated within the class where
they are declared.This is the common use case I think many envision, but nothing in the proposal requires that. A public write-once property (as currently written) would be world-readable, and world-writeable, once.
Separate visibility for internal and external access is a separate matter. (Also potentially useful, but not part of the write-once proposal at the moment.)
This just hit me, so I think I will mention it.
The culture on the list seems not to be one of collaboration on RFC to find solutions to improve PHP but instead waiting for someone to stick their neck out with an RFC and then see who can shoot the most holes through it.
I did not actually expect that. I would have hoped for a collaborate culture of the nature I am used to in local startup-oriented meetups where most everyone is trying to help each other rather than just pointing out that the work someone else has done is deficient and not worthy of moving forward.
If that is the desired culture that most want to have on this list, then I will accept it even though I will lament how much better it could be otherwise.
If I misunderstand, please help me understand how wording like "but nothing in the proposal requires that" and "not part of the write-once proposal at the moment" rather than "the proposal does not require that but lets's explore how adding that requirement can make the proposal better" should not leave the impression I just mentioned above?
Or maybe the problem is the mailing list is just not a mechanism for collaborative work and we need a new mechanism. Like Github, comments, and pull requests?
Moving on...
- If you want to update a property from outside the class, create a
set_<property>() method to allow it to happen.- If you do not want it to be set externally, do not implement a
set_<property>() method.- If you want it to be implemented externally sometimes but not
others, implement guard classes inside the set_<property>() method.I think that addresses all scenarios, no?
-Mike
It does not.
- Race condition if I assume that a public write-once property is a materialized value, but access it before it gets materialized.
Can you please explain what "materialized value" means in this context? Are you speaking in Scala again?
If you mean that a write-once property just has not yet been assigned, I am not sure how that differs from just accessing a property that has not yet been assigned today?
- Race condition if internal non-constructor code wants to set the value, but some external routine has set it first.
As I said, don't let the external routine set it directly. Which you did note.
(I am interested in finding solutions in the same solution space as selected proposals and not just limiting myself to the exact specifics of the proposal because IMO limiting ourselves in that way has all the bad aspects of bureaucracy and none of the good aspects.)
- Cloning creates an interesting and complicated case of both of the above. Does a cloned object start with its write-once bits reset or no? There's problems both ways.
If there are problems, let us find solutions.
- Only allow internal methods to change write-once properties.
- Options:
2a. Extend clone statement to haveclone rewrite $foo
that would allow rewriting the write-once properties.
2b. Allow rewriting once of write-once object properties until some operation is performed to seal or lock the object (operation being a method call, a function call with the object as param, or a statement with the object as argument.)
2c. Don't allow rewriting of cloned objects. See if it is really a problem. Fix it in a future RFC if so.
2d. Something else I am someone we can think of.
-Mike
I'm totally on board for better value object support, so that's a good motive for me. The question I have is whether this is really a good stepping stone in that direction or if it would lead down a wrong path and lock us into too much TIMTOWTDI (for the Perl fans in the room). So let's think that through down that path. How would write-once properties lead into properly immutable value objects? Or do they give us that themselves?
The biggest challenge for immutable objects, IMO, is evolving them. Eg, $result->withContentType(...) to use the PSR-7 example. Would we expect people to do it with a method like that, or would there be some other mechanism? If the properties are public, would we offer a more syntactic way to modify them directly?
The with*() method style requires cloning the object. What happens to the locked status of a set property if the object is cloned? Are they then settable again, or do they come pre-locked?
Neither of those seem good, now that I think about it. If they come pre-locked, then you really can't clone, change one property, and return the new one (as is the standard practice now in that case). If they don't come pre-locked, then the newly created object can have everything on it changed, once, which creates a loophole. I'm not sure what the right answer is here.
My other concern is a public property (the most likely use case) would have to be set in the constructor. If it's not, then callers cannot rely on it having been set yet if it's set lazily. And if code inside the class tries to set it lazily, it may already have been set by some external code (rightly or wrongly) and cause a failure.
How do we address that? There's absolutely use cases where setting everything in the constructor ahead of time is what you'd do anyway, but there are plenty where you wouldn't want to, either, which creates a race condition for who sets it first, or tries to access it before it gets set, etc. (This is where my repeated questions about lazy initialization come from.)
I have struggled to follow this RFC thread fully, so if I am getting
something out of context, please note that and I apologize in advance.However, it would see that rules for
write once
properties to support
lazy loading would be rather simple:
- Write-once properties can only be updated once.
- Write-once properties can only be updated within the class where
they are declared.This is the common use case I think many envision, but nothing in the proposal requires that. A public write-once property (as currently written) would be world-readable, and world-writeable, once.
Separate visibility for internal and external access is a separate matter. (Also potentially useful, but not part of the write-once proposal at the moment.)This just hit me, so I think I will mention it.
The culture on the list seems not to be one of collaboration on RFC to
find solutions to improve PHP but instead waiting for someone to stick
their neck out with an RFC and then see who can shoot the most holes
through it.I did not actually expect that. I would have hoped for a collaborate
culture of the nature I am used to in local startup-oriented meetups
where most everyone is trying to help each other rather than just
pointing out that the work someone else has done is deficient and not
worthy of moving forward.
Mike,
Terje already responded here and he is spot on: "Destructive testing" is a very good metaphor to use, and is what is happening in this thread.
Evolving a language like PHP, with millions of users and billions of lines of code, is not, even a little, like a startup-oriented meetup. In a meetup, "yes and" is a perfectly good model, and you can encourage everyone to make their own thing, throw it out into the market, and see what happens. Multiple incompatible and competing startups can succeed and the ecosystem is better for it.
But there is only one PHP language, and throwing everything and the kitchen sink into it just to make people feel good and inspired is a superb way to ruin the language. Every new feature or syntax addition creates cognitive overhead for millions of people. If done in a way that has lots of holes or flaws, it can create billion-dollar bugs that take a decade to unravel, if ever. The cost of "getting it wrong" is high. The cost/benefit analysis has to be very strong to justify adding syntactic weight to every PHP user in the world.
That does not mean that we should oppose all change; hell, PHP is perhaps the fastest evolving production language in the world right now. There's a long list of recent features I've supported and cheered for, and a long list I'd still like to see. The same is true of almost everyone on this list.
But in order to keep PHP a good, approachable, flexible language and keep it consistent (well, at least no more inconsistent than it already is), every one of those features needs to "run the gauntlet" of destructive testing, poking holes in it, and finding all the gotchas. That's exactly the point: We do want to find all the little gotchas in a proposal, now, before something gets into the language and the PHP-coding world finds them instead.
Constructive nitpicking and destructive testing, in this context, are a sign of respect. If folks on the list didn't respect this proposal, it would have been met with "this is stupid", "we went over this, it's dumb", "why are you wasting our time?", or other such responses. (Certainly there have been proposals met with such response in the past.) Or, simply ignored outright. Instead, it's being met with "Hm, someone is going to try and throw this against a wall harder than they have any right to; so let's do it now and see what happens so we can figure out how to make it not shatter into a thousand pieces." That's a good thing. Numerous people are donating a not-small amount of their time to stress-test the proposal.
Some of that work has already improved the proposal, eg, the general agreement that it was best to just ignore untyped properties and references entirely. That's a good thing.
Máté has said that his end goal is immutable objects, and this is a step in that direction. I fully support that goal. Is this actually the right first step in that direction, or does it go down a wrong path? I don't know yet; that's what all of the nitpicking and hypotheticals and "running it over with a car to see what happens" is about: Figuring out if it is the right way to get to that goal. I'm glad we're having this conversation.
Maybe a readonly flag only makes sense if we first/also add separate visibility for getters and setters? I don't know, but it's worth asking. Does it break cloning? Seems like it, but maybe there's a way around it. Would some totally different approach get to a better end-state with fewer edge cases? Could be. Those are realizations that only happen when you take an RFC and "shoot the most holes through it", because then you find the places where it's not bulletproof. That's the process working as designed.
- Race condition if I assume that a public write-once property is a materialized value, but access it before it gets materialized.
Can you please explain what "materialized value" means in this context?
Are you speaking in Scala again?
I don't speak Scala. If anything it's an SQL reference. A "materialized table" is basically a saved query result, which you can then reuse over and over again, that self-updates when its underlying source changes.
Think $user->fullName, where fullName is a public read-only property derived by concatenating $firstName and $lastName. If that is set in the User constructor, great, no problem. If it's not, because it's not always needed so you want to only do that work if it's going to be used, then the caller doesn't know if it's already been set or if it's still undefined. Or, if public write isn't blanket-disallowed (which could be done but I suspect would lead to other limitations), whatever internal code would set the value doesn't know if it's already been set by an over-eager external routine (possibly incorrectly).
If you mean that a write-once property just has not yet been assigned,
I am not sure how that differs from just accessing a property that has
not yet been assigned today?
It doesn't, which is one of the reasons public properties are broadly frowned upon today. However, most of the use cases of a readonly flag I can see apply to publicly-readable properties (which are therefore faster to access than a method and involve less boilerplate code). So if a readonly flag doesn't resolve that problem, and in practice makes it a bit worse, that puts it at a net-negative ROI.
I keep coming back to supporting lazy-initialization because that seems to me the best way to resolve that problem, and gives us some very clean additional functionality. Are there other options that are better? Could well be. Let's enumerate them and poke holes in all of those, too.
- Cloning creates an interesting and complicated case of both of the above. Does a cloned object start with its write-once bits reset or no? There's problems both ways.
If there are problems, let us find solutions.
- Only allow internal methods to change write-once properties.
Possible; which then leads to the question "why can't every property have asymmetric access?" Which someone else upthread mentioned, and IMO warrants its own discussion.
- Options:
2a. Extend clone statement to haveclone rewrite $foo
that would
allow rewriting the write-once properties.
In the common case, you'd only want to overwrite a subset of properties, probably only one. How do you "unlock" just that one property? Is that the right way to go about it? (I honestly don't know.)
2b. Allow rewriting once of write-once object properties until some
operation is performed to seal or lock the object (operation being a
method call, a function call with the object as param, or a statement
with the object as argument.)
My preference here would be the "overwrite code block" Rowan suggested. That may have other knock-on effects (either for consistency or engine complexity) that we haven't thought of yet.
2c. Don't allow rewriting of cloned objects. See if it is really a
problem. Fix it in a future RFC if so.
Possible, but also then removes one of the primary use cases for the feature: Immutable value objects, since then evolving them becomes far more work than it is now.
--Larry Garfield
My original idea was to discuss cloning in connection with immutable
objects, - I think it's only a serious problem in that case - but we can
bring this discussion earlier of course, as readonly properties certainly
has some effect on the clonability of objects.
When I started working on implementing immutable objects, I tried to come
up with a solution for the problem of property mutation. Actually, I even
had a short discussion with Nikita about the topic. He proposed the
following syntax (which is inspired by Rust):
public function withFoo(FooT $foo): static {
return new static { foo => $foo, ...$this };
}
My idea was very similar, but it affects cloning:
$self = clone $this with { foo => $foo, ... }
I also tried to experiment with similar solutions to the ones proposed by
Rowan, where readonly properties were unsealed after cloning (in certain
circumstances), but my general feeling was these are not the way to go. To
be honest, I also don't really like object initializers because they omit
invoking the constructor. However, "beefing up" cloning would make sense
for me.
Given, adding support for object initializers (or a special for of it), is
also a non-trivial problem - as seen in case of the previous RFC -, and
given, the absence of this construct is not a deal-breaker for readonly
properties, I still think that it would be worth to separate the two topic
(and vote separately). This way, we can keep the current proposal
self-contained and focused. If the current vote passed, I'd certainly try
to address the problem of property mutation after cloning in the immutable
object proposal.
Regards,
Máté
Sorry, but I'd like to add a correction to my previous email.
After thinking about it, I realized that cloning is not that serious of a
problem in case of immutable objects, since it's also possible to create a
new instance instead of cloning. So it seems to be only a Developer
Experience concern for me - as cloning is more comfortable to use than
passing all the properties to a new object.
Regards,
Máté
The with*() method style requires cloning the object. What happens to the
locked status of a set property if the object is cloned? Are they then
settable again, or do they come pre-locked?Neither of those seem good, now that I think about it. If they come
pre-locked, then you really can't clone, change one property, and return
the new one (as is the standard practice now in that case). If they don't
come pre-locked, then the newly created object can have everything on it
changed, once, which creates a loophole. I'm not sure what the right
answer is here.
As with typed properties, I wonder if there's a way we can introduce a new
initialisation sequence for objects, so that there's a specific point where
the object is considered "fully constructed" after new or clone.
A couple of brainstormed ideas, with plenty of downsides I'm sure:
An explicit finalise() function or keyword
public function withFoo($foo) {
$inst = clone $this;
// all readonly properties are initially "unlocked"
$inst->foo = $foo;
// now lock them, perhaps also checking that no typed properties are
left uninitialised
finalise($inst); // or finalise $inst;
return $inst;
}
A special code block:
public function withFoo($foo) {
$inst = clone $this {
// all properties are "unlocked" within this special block
$inst->foo = $foo;
};
// from here onwards, readonly properties can't be written to
return $inst;
}
Perhaps could also be used with constructors:
public function createFromOtherThing(OtherThing $other) {
$inst = new static('some parameter') {
// readonly properties can be written in the constructor, or
within this block
$inst->foo = $other->getFoo();
};
// object is "finalised" when the block ends
return $inst;
}
Regards,
Rowan Tommins
[IMSoP]
The with*() method style requires cloning the object. What happens to the
locked status of a set property if the object is cloned? Are they then
settable again, or do they come pre-locked?Neither of those seem good, now that I think about it. If they come
pre-locked, then you really can't clone, change one property, and return
the new one (as is the standard practice now in that case). If they don't
come pre-locked, then the newly created object can have everything on it
changed, once, which creates a loophole. I'm not sure what the right
answer is here.As with typed properties, I wonder if there's a way we can introduce a new
initialisation sequence for objects, so that there's a specific point where
the object is considered "fully constructed" after new or clone.A couple of brainstormed ideas, with plenty of downsides I'm sure:
An explicit finalise() function or keyword
public function withFoo($foo) {
$inst = clone $this;
// all readonly properties are initially "unlocked"
$inst->foo = $foo;
// now lock them, perhaps also checking that no typed properties are
left uninitialised
finalise($inst); // or finalise $inst;
return $inst;
}A special code block:
public function withFoo($foo) {
$inst = clone $this {
// all properties are "unlocked" within this special block
$inst->foo = $foo;
};
// from here onwards, readonly properties can't be written to
return $inst;
}Perhaps could also be used with constructors:
public function createFromOtherThing(OtherThing $other) {
$inst = new static('some parameter') {
// readonly properties can be written in the constructor, or
within this block
$inst->foo = $other->getFoo();
};
// object is "finalised" when the block ends
return $inst;
}
If the way to resolve this question is a special "unlocked" mode, I would definitely favor the explicit code block. That way it's self-closing and you can't forget to do so. (Murphy's Law: If you rely on developers remembering to do X to keep code safe, they will promptly forget to do X.)
Also, FTR, any approach that forces developers to write a 9 parameter constructor over and over is one I can never get behind, doubly so for something that is currently only a single line. This isn't a case of "pain tells you what not to do"; a value object having a lot of internal properties is a completely valid use case, and "but composition" is not an answer.
--Larry Garfield
Le lun. 24 févr. 2020 à 21:35, Larry Garfield larry@garfieldtech.com a
écrit :
On Fri, 21 Feb 2020 at 23:18, Larry Garfield larry@garfieldtech.com
wrote:The with*() method style requires cloning the object. What happens to
the
locked status of a set property if the object is cloned? Are they then
settable again, or do they come pre-locked?Neither of those seem good, now that I think about it. If they come
pre-locked, then you really can't clone, change one property, and
return
the new one (as is the standard practice now in that case). If they
don't
come pre-locked, then the newly created object can have everything on
it
changed, once, which creates a loophole. I'm not sure what the right
answer is here.As with typed properties, I wonder if there's a way we can introduce a
new
initialisation sequence for objects, so that there's a specific point
where
the object is considered "fully constructed" after new or clone.A couple of brainstormed ideas, with plenty of downsides I'm sure:
An explicit finalise() function or keyword
public function withFoo($foo) {
$inst = clone $this;
// all readonly properties are initially "unlocked"
$inst->foo = $foo;
// now lock them, perhaps also checking that no typed properties are
left uninitialised
finalise($inst); // or finalise $inst;
return $inst;
}A special code block:
public function withFoo($foo) {
$inst = clone $this {
// all properties are "unlocked" within this special block
$inst->foo = $foo;
};
// from here onwards, readonly properties can't be written to
return $inst;
}Perhaps could also be used with constructors:
public function createFromOtherThing(OtherThing $other) {
$inst = new static('some parameter') {
// readonly properties can be written in the constructor, or
within this block
$inst->foo = $other->getFoo();
};
// object is "finalised" when the block ends
return $inst;
}If the way to resolve this question is a special "unlocked" mode, I would
definitely favor the explicit code block. That way it's self-closing and
you can't forget to do so. (Murphy's Law: If you rely on developers
remembering to do X to keep code safe, they will promptly forget to do X.)Also, FTR, any approach that forces developers to write a 9 parameter
constructor over and over is one I can never get behind, doubly so for
something that is currently only a single line. This isn't a case of "pain
tells you what not to do"; a value object having a lot of internal
properties is a completely valid use case, and "but composition" is not an
answer.
I totally agree with this: there must be a way to work around the keyword -
either via reflection or another means.
Unlike private
, final
is an imperative keyword that cannot be bypassed,
while there are very legit use cases for still extending final classes
(proxies). For sure, the same will be legitimately needed with readonly
.
Via Reflection, it could be a new method ->setWritable(true)
(next to
->setAccessible(true)
).
Another way, which is my current preference, would be to have visibility be
taken into consideration when using the keyword:
-
public readonly $foo
=> cannot be set from the outside of the class -
but can be from protected+private scopes -
protected readonly $foo
=> can be set only from private scope -
private readonly $foo
=> either unsupported or have thewriteonce
behavior defined in the RFC.
This would make the keyword compatible with untyped properties and with
cloning.
It would provide the guarantee we need from an author pov: outside scopes
cannot mess up with the state of properties. It would not provide authors a
safeguard against their own mistakes - but I think that's a really
secondary goal and one that is fine letting go vs the mentioned benefits.
Nicolas
I totally agree with this: there must be a way to work around the keyword -
either via reflection or another means.
Via Reflection, it could be a new method ->setWritable(true)
(next to
->setAccessible(true)
).
I'm OK to support working around the keyword via reflection. Since my
implementation uses a property flag to determine if a property is writable,
and the flag is flipped after writing, the ->setWritable(true) wouldn't
work. But
I can imagine adding a ->setUninitialized() method, or adding a third
parameter to ->setValue() which would affect whether the property flag
should be
first reset or not. Adding a separate ->setValue() method for this purpose
is also
possible, but I couldn't find a good name for that yet. What do you think?
Another way, which is my current preference, would be to have visibility be
taken into consideration when using the keyword:
To be honest, I don't agree with this idea. It is rather a simpler property
accessor
variant with which one can define separate - but predefined - visibility
rules for
reading and writing.
Furthermore, I think it's worth to protect people from their own mistakes.
I know
for sure that at least I'd need those safeguards. :) But if we are talking
about
unintended changes coming from outside scopes, and if we only consider
"legal" use-cases, private and protected properties are already pretty much
safe.
However, if we also consider possible changes coming from an inside scope,
and other, "illegal" use-cases, we currently don't have any protection. And
not
even property accessors or your idea could help here.
Given, immutability became mainstream via PSR-7 at least, I think it would
be
beneficial to add first-class support for this principle. I remember that
one of the
criticisms against the implementation of PSR-7 was that immutability is not
even
possible to achieve in PHP (apart from the fact that PSR-7 uses a mutable
resource for storing the body). Now, we could solve the first problem, but
that
requires us to stay with the proposed implementation of the RFC.
Regards,
Máté
TL;DR; I plan to open the vote on Monday because the proposal feels complete
for me. Read more to find out why I think so.
Nicolas, do you have any specific use-case in mind that would require a
workaround
outlined in my previous email above the current possibilities (lazy
initialization,
unsetting before first write)?
At first, I wasn't entirely sure if we really need additional reflection
methods, but after
a quick chat with Nikita, I'm even less so. Furthermore, it seems that
ProxyManager
will be able to work together with my proposal in its current form, so I
fail to see
what use-case would need more special treatment? Doctrine maybe? Although I
am
not very familiar with it, using
ReflectionClass::newInstanceWithoutConstructor()
could work around the limitations imposed by write-once properties.
That said, I'd like to start the vote on Monday if no major issues emerge
meanwhile,
because the proposal feels complete now and I can't think of any specific
use-case
that would be blocked by write-once properties. Of course, if we yet find
any, we can
always add support for additional reflection-based workarounds later.
Máté
As you might noticed, I've not opened the vote yet. Partly because I was
improving
my implementation as well as the RFC itself (added some words about the
inheritance
implications), but the main reason is that a question arise in the
meanwhile.
Namely, "write-once" properties could in principle support covariance. That
is, a subclass
would be allowed to tighten the property type that is inherited from the
parent class.
It would be a slight change compared to regular properties that are
invariant.
All this would be possible because of the quasi-immutable nature of
"write-once" properties:
they are generally expected to be assigned to only once, in the constructor
- which is exempt from
LSP checks.
There is a gotcha though... In practice, "write-once" properties could be
written from places
other than the constructor. Although there might not be many practical
use-cases for it,
the infamous setter injection is certainly one (as shown at
https://3v4l.org/DQ3To), in which
case property covariance would be a problem.
That's why I'm curious about some additional input on the matter. Do you
think
covariance of "write-once" properties is worth to have even though there
might be
some edge-cases when it can't be supported perfectly? I'll include this
topic a bit later
in the RFC as well. In the worst case it could be added to the "Future
Scope" section
because - and correct me if I'm wrong - we can also add support for it
later since it would be
a non-breaking change.
Cheers,
Máté
As you might noticed, I've not opened the vote yet. Partly because I was
improving
my implementation as well as the RFC itself (added some words about the
inheritance
implications), but the main reason is that a question arise in the
meanwhile.Namely, "write-once" properties could in principle support covariance. That
is, a subclass
would be allowed to tighten the property type that is inherited from the
parent class.
It would be a slight change compared to regular properties that are
invariant.All this would be possible because of the quasi-immutable nature of
"write-once" properties:
they are generally expected to be assigned to only once, in the constructor
- which is exempt from
LSP checks.There is a gotcha though... In practice, "write-once" properties could be
written from places
other than the constructor. Although there might not be many practical
use-cases for it,
the infamous setter injection is certainly one (as shown at
https://3v4l.org/DQ3To), in which
case property covariance would be a problem.That's why I'm curious about some additional input on the matter. Do you
think
covariance of "write-once" properties is worth to have even though there
might be
some edge-cases when it can't be supported perfectly? I'll include this
topic a bit later
in the RFC as well. In the worst case it could be added to the "Future
Scope" section
because - and correct me if I'm wrong - we can also add support for it
later since it would be
a non-breaking change.Cheers,
Máté
I'd strongly suggest not messing with that for now. There's a definite can of worms, not all of which we likely know about yet. Plus, as you say, it's easier to add support for that later if we can find all of the worms than to try and put those worms back in the box after they've gotten loose.
--Larry Garfield
Thank you, Larry, for your response! I share your opinion. However, I'd be
curious if there is anyone who doesn't?
As things currently stand, I plan to start the vote on Monday with an
unchanged proposal (+ an extended future scope section).
Máté
Hey Máté,
Is the RFC still gonna allow default values (constants, at this point)?
While I don't see a major problem with it, it seems a bit weird...
Thank you, Larry, for your response! I share your opinion. However, I'd be
curious if there is anyone who doesn't?As things currently stand, I plan to start the vote on Monday with an
unchanged proposal (+ an extended future scope section).Máté
Hi Marco,
Yes, it still allows default values.
The reason why I'm reluctant to disallow them is that this restriction
would feel a bit ad-hoc for me. I mean, I wouldn't like to add another
special rule for "write-once" properties, unless there is a strong
argument for it. Besides, as far as I know there is no precedents of
disallowing default values of similar properties in other languages,
so I feel that the feature would stay the most intuitive as it is now.
However, I'm eager to listen any objections about this. I know for one
that ProxyManager wouldn't work with "write-once" properties having
default values. But can we consider this use-case an edge case, right?
Could users circumvent the issue just by changing the default value
to an assignment in the constructor? Or would it cause a big headache
for them?
Máté
Marco Pivetta ocramius@gmail.com ezt írta (időpont: 2020. márc. 14., Szo,
22:53):
Hey Máté,
Is the RFC still gonna allow default values (constants, at this point)?
While I don't see a major problem with it, it seems a bit weird...
Thank you, Larry, for your response! I share your opinion. However, I'd be
curious if there is anyone who doesn't?As things currently stand, I plan to start the vote on Monday with an
unchanged proposal (+ an extended future scope section).Máté
Hey Máté,
Hi Marco,
Yes, it still allows default values.
The reason why I'm reluctant to disallow them is that this restriction
would feel a bit ad-hoc for me. I mean, I wouldn't like to add another
special rule for "write-once" properties, unless there is a strong
argument for it. Besides, as far as I know there is no precedents of
disallowing default values of similar properties in other languages,
so I feel that the feature would stay the most intuitive as it is now.
I think what will happen is that people will start requesting for read-only
properties with default values to be over-writable-once (a mess): better to
remove them from the equation completely, no?
However, I'm eager to listen any objections about this. I know for one
that ProxyManager wouldn't work with "write-once" properties having
default values. But can we consider this use-case an edge case, right?
Yeah, that's totally unrelated to library technicalities: since reflection
will tell us if a property is write-once, we can work around it regardless.
Could users circumvent the issue just by changing the default value
to an assignment in the constructor? Or would it cause a big headache
for them?
I would say that this complicates the semantics more, but you see that the
issue is indeed a bit more deep than what we wanted to tackle, so I think a
sensible solution is to:
- Prevent the parser from accepting default values on write-once
properties (parser error) - Re-introduce them once we know what we want to do with default values
(and it makes sense)
We can then use the fact that nobody is relying on them to play with the
future scope.
Nikic suggested (in chat) an example usage such as default non-scalar
values as viable:
class Application
{
public read-only DateTimeImmutable $startupTime = new DateTimeImmutable
();
// ...
}
Hey Máté,
Hi Marco,
Yes, it still allows default values.
The reason why I'm reluctant to disallow them is that this restriction
would feel a bit ad-hoc for me. I mean, I wouldn't like to add another
special rule for "write-once" properties, unless there is a strong
argument for it. Besides, as far as I know there is no precedents of
disallowing default values of similar properties in other languages,
so I feel that the feature would stay the most intuitive as it is now.I think what will happen is that people will start requesting for read-only
properties with default values to be over-writable-once (a mess): better to
remove them from the equation completely, no?
My concern is that if a readonly property can have a default value which is not overwriteable, then it's conceptually isomorphic to a class constant. That will lead to ample questions and debates about whether you should use a class constant or a read-only property, or when you should use one or the other. I foresee numerous bikeshed debates about that, which will only lead to thousands of hours of lost time debating something that shouldn't exist.
Avoiding that confusion will save the industry millions of dollars.
--Larry Garfield
Avoiding that confusion will save the industry millions of dollars.
On the one hand, you are right, because currently it's not very useful to
effectively provide two
ways of declaring a constant. On the other hand however, if we also
consider a longer term
aim of adding support for object default values (just like what Marco
mentioned):
public read-only DateTimeImmutable $startupTime = new DateTimeImmutable();
then allowing default values for "write-once" properties seems much more
sensible. At this point,
the "million dollar mistake" label doesn't hold anymore since class
constants and "write-once"
properties with default values will actually be two different things. But
we've just ended up at
Marco's suggestion:
- Prevent the parser from accepting default values on write-once
properties (parser error)- Re-introduce them once we know what we want to do with default values
(and it makes sense)
Yes, this scenario definitely makes sense. I'm just not yet sold that it
will have any negative effects
if we don't restrict the usage of default values now. I understand that
it's usually advantageous to be
conservative with adding new features - especially when groping in the dark
- since we are the ones
who have to support and fix them later. That's why I was hesitant to add
property covariance to the
proposal.
But this case seems to be much more well-understood than let's say property
covariance would be,
it avoids another special rule while aligning nicely with our longer term
goals, so I think all in all, it is still a
better idea to allow default values than not.
I think what will happen is that people will start requesting for read-only
properties with default values to be
over-writable-once (a mess): better to remove them from the equation
completely, no?
I can also imagine people to ask for default values if we disallow them
now. :) After all, we always want
what we don't have. ^^
Does anyone else have any more comments or objections? I wouldn't in the
least want to be stubborn about
this topic, so if anyone has any feelings for/against, it's the best time
to reassure my point of view or
change my mind. :)
Thanks,
Máté
Avoiding that confusion will save the industry millions of dollars.
On the one hand, you are right, because currently it's not very useful to
effectively provide two
ways of declaring a constant. On the other hand however, if we also
consider a longer term
aim of adding support for object default values (just like what Marco
mentioned):
public read-only DateTimeImmutable $startupTime = new DateTimeImmutable();
then allowing default values for "write-once" properties seems much more
sensible. At this point,
the "million dollar mistake" label doesn't hold anymore since class
constants and "write-once"
properties with default values will actually be two different things. But
we've just ended up at
Marco's suggestion:
I'll be honest, I really have no idea how default object values is at all related here. I mean, those would be nice to have as well for various reasons, but I don't see how it's related to read-only properties.
- Prevent the parser from accepting default values on write-once
properties (parser error)- Re-introduce them once we know what we want to do with default values
(and it makes sense)Yes, this scenario definitely makes sense. I'm just not yet sold that it
will have any negative effects
if we don't restrict the usage of default values now. I understand that
it's usually advantageous to be
conservative with adding new features - especially when groping in the dark
- since we are the ones
who have to support and fix them later. That's why I was hesitant to add
property covariance to the
proposal.
The negative effect is if we later decide that it's too much of a problem to have default values, because they're just too confusing (people debating if they should be a constant, people expecting them to be overwriteable, etc.), we can't simply remove them. That would be a large breaking change and not allowed, so we're stuck with 'em.
Whereas if we don't add it now, we can see if people are really clammoring for it and in what situations. We let the PHP-using masses do the research for us to determine how read-only-default would actually be useful, or if it would be useful at all. Then we can add it later in a way that would actually be useful, or decide not to do it at all.
--Larry Garfield
On Mon, Mar 16, 2020 at 12:23 AM Larry Garfield larry@garfieldtech.com
wrote:
Avoiding that confusion will save the industry millions of dollars.
On the one hand, you are right, because currently it's not very useful to
effectively provide two
ways of declaring a constant. On the other hand however, if we also
consider a longer term
aim of adding support for object default values (just like what Marco
mentioned):
public read-only DateTimeImmutable $startupTime = new
DateTimeImmutable();
then allowing default values for "write-once" properties seems much more
sensible. At this point,
the "million dollar mistake" label doesn't hold anymore since class
constants and "write-once"
properties with default values will actually be two different things. But
we've just ended up at
Marco's suggestion:I'll be honest, I really have no idea how default object values is at all
related here. I mean, those would be nice to have as well for various
reasons, but I don't see how it's related to read-only properties.
Default object values are a case where default values for readonly
properties have a legitimate use-case, because the actual value will be
different for each object. Currently only constant values are permissible,
which will thus be the same for all objects, and as such not materially
different from class constants.
A few more considerations on how readonly defaults this would interact with
potential future language changes...
What happens if we introduce a shorthand syntax for combining property
declarations, constructors and initialization?
class Point {
public function __construct(
public readonly float $x = 0.0,
public readonly float $y = 0.0,
public readonly float $z = 0.0,
) {}
}
The naive transformation for this would be:
class Point {
public readonly float $x = 0.0;
public readonly float $y = 0.0;
public readonly float $z = 0.0;
public function __construct(float $x = 0.0, float $y = 0.0, float $z =
0.0) {
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}
Which of course will not work with readonly properties as defined.
Alternatively one could transform this without the default values on the
properties themselves, only on the arguments. That does lose out on
Reflection though.
The other one is the recently declined
https://wiki.php.net/rfc/object-initializer. As it basically works by first
creating the object normally (including a possible constructor call), and
then assigning the specified properties, this would not be compatible with
readonly properties that have defaults. Other implementation approaches are
possible though, but may not be easily reconcilable with the need to also
call the constructor.
Not really sure what I'm trying to tell you with this though :)
Nikita
- Prevent the parser from accepting default values on write-once
properties (parser error)- Re-introduce them once we know what we want to do with default
values
(and it makes sense)Yes, this scenario definitely makes sense. I'm just not yet sold that it
will have any negative effects
if we don't restrict the usage of default values now. I understand that
it's usually advantageous to be
conservative with adding new features - especially when groping in the
dark
- since we are the ones
who have to support and fix them later. That's why I was hesitant to add
property covariance to the
proposal.The negative effect is if we later decide that it's too much of a problem
to have default values, because they're just too confusing (people debating
if they should be a constant, people expecting them to be overwriteable,
etc.), we can't simply remove them. That would be a large breaking change
and not allowed, so we're stuck with 'em.Whereas if we don't add it now, we can see if people are really clammoring
for it and in what situations. We let the PHP-using masses do the research
for us to determine how read-only-default would actually be useful, or if
it would be useful at all. Then we can add it later in a way that would
actually be useful, or decide not to do it at all.--Larry Garfield
The other one is the recently declined
https://wiki.php.net/rfc/object-initializer. As it basically works by
first
creating the object normally (including a possible constructor call), and
then assigning the specified properties, this would not be compatible with
readonly properties that have defaults. Other implementation approaches are
possible though, but may not be easily reconcilable with the need to also
call the constructor.
I think what I'd expect from a possible object initializer feature is that
it
can't overwrite "write-once" properties with default values. (?)
However, I'm ok to to remove support for default values because of the
possible
problems outlined (confusion for end users, uncertainty how it'd work
together with new
features etc.). It seems that the replies are more on this side, so let's
be a bit more
conservative than I originally wanted to be, so that we have more freedom
later.
Actually, I have already updated the RFC with this. Also, I made some
clarifications
in connection with serialization and the usage of resources.
Thanks:
Máté
Le lun. 16 mars 2020 à 12:52, Máté Kocsis kocsismate90@gmail.com a écrit :
The other one is the recently declined
https://wiki.php.net/rfc/object-initializer. As it basically works by
first
creating the object normally (including a possible constructor call), and
then assigning the specified properties, this would not be compatible
with
readonly properties that have defaults. Other implementation approaches
are
possible though, but may not be easily reconcilable with the need to also
call the constructor.I think what I'd expect from a possible object initializer feature is that
it
can't overwrite "write-once" properties with default values. (?)However, I'm ok to to remove support for default values because of the
possible
problems outlined (confusion for end users, uncertainty how it'd work
together with new
features etc.). It seems that the replies are more on this side, so let's
be a bit more
conservative than I originally wanted to be, so that we have more freedom
later.Actually, I have already updated the RFC with this. Also, I made some
clarifications
in connection with serialization and the usage of resources.
I repeat what I wrote before but all those problems would disappear if we
were to bind the proposal to visibility:
https://externals.io/message/108675#108753
We could even consider splitting "read" and "write" in two separate
keywords, each bound to visibility, isn't it?
Nicolas
Le lun. 16 mars 2020 à 12:52, Máté Kocsis kocsismate90@gmail.com a écrit :
The other one is the recently declined
https://wiki.php.net/rfc/object-initializer. As it basically works by
first
creating the object normally (including a possible constructor call), and
then assigning the specified properties, this would not be compatible
with
readonly properties that have defaults. Other implementation approaches
are
possible though, but may not be easily reconcilable with the need to also
call the constructor.I think what I'd expect from a possible object initializer feature is that
it
can't overwrite "write-once" properties with default values. (?)However, I'm ok to to remove support for default values because of the
possible
problems outlined (confusion for end users, uncertainty how it'd work
together with new
features etc.). It seems that the replies are more on this side, so let's
be a bit more
conservative than I originally wanted to be, so that we have more freedom
later.Actually, I have already updated the RFC with this. Also, I made some
clarifications
in connection with serialization and the usage of resources.I repeat what I wrote before but all those problems would disappear if we
were to bind the proposal to visibility:
https://externals.io/message/108675#108753We could even consider splitting "read" and "write" in two separate
keywords, each bound to visibility, isn't it?
How would 2 separate keywords work, syntactically/visually? I can see how it would solve a larger cluster of use cases, but I'm not sure what the code would look like. :-)
--Larry Garfield
I repeat what I wrote before but all those problems would disappear if we
were to bind the proposal to visibility:
https://externals.io/message/108675#108753We could even consider splitting "read" and "write" in two separate
keywords, each bound to visibility, isn't it?How would 2 separate keywords work, syntactically/visually? I can see how
it would solve a larger cluster of use cases, but I'm not sure what the
code would look like. :-)
Hum dunno, I throw the idea to fish for interest and now I can not figure
out an answer to your question... :P
Back to my previous suggestion on my side, which provides the core of the
benefits we strive for IMHO...
Nicolas
I repeat what I wrote before but all those problems would disappear if we
were to bind the proposal to visibility:
https://externals.io/message/108675#108753We could even consider splitting "read" and "write" in two separate
keywords, each bound to visibility, isn't it?How would 2 separate keywords work, syntactically/visually? I can see how
it would solve a larger cluster of use cases, but I'm not sure what the
code would look like. :-)Hum dunno, I throw the idea to fish for interest and now I can not figure
out an answer to your question... :P
Back to my previous suggestion on my side, which provides the core of the
benefits we strive for IMHO...Nicolas
My best off-the-cuff thought would be compound name keywords.
readpublic writeprotected string $foo
Or something like that. Which... looks kind of ugly since I don't think we can do kebab case keywords. :-)
--Larry Garfield
I think what will happen is that people will start requesting for read-only
properties with default values to be over-writable-once
Exactly, I think that intuitively, developers will not see a default
value as an actual "write".
They will expect to be able to overwrite it once.
I think what will happen is that people will start requesting for
read-only
properties with default values to be over-writable-onceExactly, I think that intuitively, developers will not see a default
value as an actual "write".
They will expect to be able to overwrite it once.
I'm sure different people will react differently, but my intuition is quite
the opposite: I would probably call the inline assignment to the property
an "initial value", not a "default value", and I would intuitively compare
it to assigning it in the constructor. I would also understand the intent
of "write once" to be "once initialized, can't be overwritten", so would
personally have no expectation that I could initialize a variable in both
the property definition and the constructor.
Regards,
Rowan Tommins
[IMSoP]
I'm sure different people will react differently, but my intuition is quite
the opposite: I would probably call the inline assignment to the property
an "initial value", not a "default value", and I would intuitively compare
it to assigning it in the constructor. I would also understand the intent
of "write once" to be "once initialized, can't be overwritten", so would
personally have no expectation that I could initialize a variable in both
the property definition and the constructor.
Interesting, so if I understand you correctly, you think of a property with an
initial/default value to be initialized before the constructor, but
a property without it is
initialized in the constructor or later (at first assignment of value)?
Is this concept of initialized/non-initialized properties something that is
more or less well-described in the language? If so, I'd like to read it.
Seems like a fuzzy concept to me so far, unfortunately...
As with typed properties, I wonder if there's a way we can introduce....
Now you are talking! This gives me hope after my dejected last comment on the list.
-Mike
Of course, that does leave the question of how often you need one or the
other. Maybe just the asymmetric visibility is sufficient for most
practical purposes, in which case it may not be worthwhile to introduce
readonly properties as a separate feature.
The examples shown in my previous email are indeed not very practical, but
still, I would say that the added protection against possible misuse or
accidental modifications (coming from either inside or outside) would be
useful.
Maybe it would make more sense to forbid readonly properties with default
values?
As I mentioned in my response to Larry, my point of view is that default
values should be allowed. If there is a big opposition against this, I'm
open for a change though.
Regarding the keyword choice, I think you can drop "sealed" from the list,
as it is an established term that affects inheritance, not mutability. Of
the choices you present, "immutable", "readonly" and "writeonce" seem like
the most viable candidates.
Thank you for the suggestions! Sure, we can drop "sealed", and I'm ok to
add "immutable" and "readonly" to the list of voting choices. I'll also
extend the evaluations with your thoughts.
Regard,
Máté