On Fri, May 31, 2024, at 12:04 PM, Alexandru Pătrănescu wrote:
> On Fri, May 31, 2024 at 10:30 AM Claude Pache <claude.pa...@gmail.com> wrote:
>> 
>> 
>>> Le 30 mai 2024 à 17:07, Derick Rethans <der...@php.net> a écrit :
>>> 
>>>> 
>>>> Now, if I define the property as public private(set) with similar 
>>>> intentions, to make sure that there is no way for external scope or 
>>>> extending classes scope to write to the property, while allowing 
>>>> reading from external scope (or extending classes scope).
>>>> 
>>>> But the problem is that an extending class can define the property as 
>>>> public protected(set), and that will easily allow the property that I 
>>>> wanted to make sure it is private for writing to be changed by an 
>>>> extending class to be protected.
>>> 
>>> public private(set) properties aren't really private, so you don't get 
>>> the shadowing, but you do have a point wrt to the expectation that an 
>>> inherited class can't easily override the private(set) part (with 
>>> protected(set) or public(set)).
>> 
>> 
>> Note that the issue already exists today with readonly properties: those are 
>> basically private(set); but if you redeclare a non-private readonly property 
>> in a subclass, you can in fact initialise it from the subclass bypassing the 
>> initial private(set) restriction of the superclass: https://3v4l.org/9AV4r
>
> Yes, thank you; that's a good point.
> Seems like another issue with readonly, but then again, the 
> private(set) part of readonly is not really explicitly designed, I 
> guess.
> 
>> 
>> If you want a property not to be overridable, end of story, you can mark it 
>> as `final` (the final marker for properties was added as part of the hooks 
>> RFC, but it works also with non-hooked properties).
>
> Yes, it seems like a good enough option, and use "final public 
> private(set)" to ensure only the current class will be able to set the 
> value.
> If we all agree, I think this should be documented in the RFC.
>
> There is another small problem, I think, with "final" modifier not 
> being allowed for constructor-promoted properties.
> And maybe we can have this fixed in the current RFC if this ends up 
> being "the correct" way to define public-read private-write properties.
>
> Regards,
> Alex

Mm, yeah, this is an interesting corner case we'd not thought of.  We spent a 
little time looking at how other languages handle this.

Kotlin just disallows using private-set on a non-final property.  (Kotlin 
properties are final by default, unless otherwise specified.)  cf: 
https://kotlinlang.org/spec/inheritance.html#overriding

C# disallows changing property visibility in child classes entirely.  cf: 
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/restricting-accessor-accessibility#access-modifiers-on-overriding-accessors

Swift is... weird.  If you have a public private(set) property, you can 
override it by turning it into a virtual property with only a get hook.  If you 
also define a set hook, though, it just never gets called.  It doesn't appear 
that you can widen the set visibility, I think.

So since we now have final properties (yay, hooks RFC!), we could probably 
mostly solve this by just only allowing private(set) on a final property, like 
Kotlin.  That's probably the easiest approach.  We can also make private(set) 
properties implicitly final to avoid lots of boilerplate.  (Having to always 
type final with private(set) every time is kinda silly.)

It would mean that proxy objects implemented using child classes with hooks 
would only work with a public protected(set) class, not with a private(set), 
but that's probably fine, and since no one is doing that quite yet, obviously, 
there's no existing code to be concerned about.

However, this also brings up another interesting issue: readonly properties (in 
8.3) DO allow redeclaration, essentially adjusting the property scope (the 
class that declares it) to make the visibility check pass. That is, the 
definition of the class it is private to changes, which is different from how 
inheritance works elsewhere.  When the parent writes to the same property, a 
special check is needed to verify the two properties are related.  All that 
special casing effectively means that readonly in 8.4 wouldn't really be "write 
once + private(set)", but "write once + private(set) - final", which is... just 
kinda screwy.  That means our options are:

* A BC break on readonly (not allowing it to be overridden)
* Make readonly an exception to the implicit final.
* Just don't allow readonly with aviz after all.

After some consideration, we believe the third option is best (least bad).  The 
other options add still-more special cases or break existing code, neither of 
which are good.  If a property is declared private(set), then child classes 
simply should not be allowed to change that, period.  Even adding a get hook in 
a child is potentially a problem, as it would change the "round trip" behavior 
in ways the parent class doesn't expect.  Disallowing readonly and making 
private(set) implicitly final avoids the loopholes in the language that would 
violate that expectation.

With aviz, all the use cases for readonly are essentially covered already.  If 
you have private(set), you can control when it gets written anyway, so if you 
want it to be write-once, it’s write once.  If you have protected(set), you’re 
allowing a child to set it, so they can do so whenever they want anyway, and 
could be doing it out of order from the parent class to begin with.

So if you wanted the equivalent of "public protected(set) readonly string 
$foo", in practice, the readonly is not giving you much to begin with.  And 
"public public(set) readonly string $foo"... we just can't really think of a 
good use case for.  Even if one exists, that could be emulated with a set hook 
now.

In terms of code length, thanks to the optional public-get visibility, the 
amount of typing is roughly the same:

readonly class ReadonlyPoint {
    public function __construct(
        public int $x,
        public int $y,
    ) {}
}

class AvizPoint {
    public function __construct(
        private(set) int $x,
        private(set) int $y,
    ) {}
}

(Or protected, if you prefer.)  The latter provides a lot more flexibility, at 
the cost of "enforce write-once yourself in the class if you care", which seems 
an entirely reasonable tradeoff.

So we feel the best way forward is to make the following changes:

* private(set) implicitly means "final".  (You can declare it explicitly if you 
want, but it isn't necessary.)
* Make readonly incompatible with aviz again.

Thoughts?

Also, Alexandru noted earlier that final properties don't seem to be supported 
in constructor promotion.  That's an oversight from the hooks implementation, 
not a design choice, so Ilija will just fix that as part of the hooks PR before 
it gets fully merged.

--Larry Garfield

Reply via email to