Re: [RFC[ Property accessor hooks, take 2

From: Date: Fri, 01 Mar 2024 06:28:08 +0000
Subject: Re: [RFC[ Property accessor hooks, take 2
References: 1 2 3 4 5 6  Groups: php.internals 
Request: Send a blank email to internals+get-122537@lists.php.net to get a copy of this message
> On 28 Feb 2024, at 21:25, Larry Garfield <larry@garfieldtech.com> wrote: > > On Wed, Feb 28, 2024, at 7:49 AM, Stephen Reay wrote: > > *snip* > >>> == Re virtual properties: >>> >>> Ilija and I talked this through, and there's pros and cons to a >>> virtual keyword. Ilija also suggested a >>> backed keyword, which forces a backed property to exist even if >>> it's not used in the hook itself. >>> >>> * Adding virtual adds more work for the developer, but >>> more clarity. It would also mean $this->$propName or $this->{__PROPERTY__} would work >>> "as expected", since there's no auto-detection for virtual-ness. On the downside, if >>> you have a could-be-virtual property but never actually use the backing value, you have an extra >>> backing value hanging around in memory that is inaccessible normally, but will still show up in some >>> serialization formats, which could be unexpected. If you omit one of the hooks and forget to mark >>> it virtual, you'll still get the default of the other operation, which could be unexpected. >>> (Mostly this would be for a virtual-get that accidentally has a default setter because you forgot to >>> mark it virtual.) >>> * Doing autodetection as now, but with an added "make a backing value anyway" >>> flag would resolve the use case of "My set hook just calls a method, and that method sets the >>> property, but since the hook doesn't mention the property it doesn't get created" >>> problem. It would also allow for $this->$propName to work if a property is explicitly backed. >>> On the flipside, it's one more thing to think about, and the above example it solves would be >>> trivially solved by having the method just return the value to set and letting the set hook do the >>> actual write, which is arguably better and more reliable code anyway. >>> * The status quo (auto-detection based on the presence of $this->propName). This >>> has the advantage it "just works" in the 95% case, without having to think about anything >>> extra. The downside is it does have some odd edge cases, like needing $this->propName to be >>> explicitly used. >>> >>> I don't think any is an obvious winner. My personal preference would be for >>> status quo (auto-detect) or explicit-virtual always. I could probably live with either, though I >>> think I'd personally favor status quo. Thoughts from others? >>> >> >> I agree that a flag to make the field *virtual* (thus disabling the >> backing store) makes more sense than a flag to make it backed; It's >> also easier to understand when comparing hooked properties with regular >> properties (essentially, backed is the default, you have to opt-in to >> it being virtual). I don't think the edge cases of "auto" make it >> worthwhile just to not need "virtual". > > Uh, I can't tell if you're saying "use status quo" or "use the virtual > keyword." Please clarify. :-) I'm saying I think an explicit virtual keyword is the better option of the three (because the benefits of the 'auto' mode don't outweigh the edge cases it introduces). > >>> == Re reference-get >>> >>> Allowing backed properties to have a reference return creates a situation where any >>> writes would then bypass the set hook, and thus any validation implemented there. That is, it makes >>> the validation unreliable. A major footgun. The question is, do we favor caveat-emptor flexibility >>> or correct-by-construction safety? Personally I always lead toward the latter, though PHP in >>> general is... schizophrenic about it, I'd say. :-) >>> >>> At this point, we'd much rather leave it blocked to avoid the issue; it's >>> easier to enable that loophole in the future if we really want it than to get rid of it if it turns >>> out to have been a bad idea. >>> >>> There is one edge case that *might* make sense: If there is no set hook defined, then >>> there's no set hook to worry about bypassing. So it may be safe to allow &get on backed >>> properties IFF there is no set hook. I worry that is "one more quirky edge case", though, >>> so as above it may be better to skip for now as it's easier to add later than remove. But if >>> the consensus is to do that, we're open to it. (Question for everyone.) >>> >> >> I don't have strong feeling about this, but in general I usually tend >> to prefer options that are consistent, and give power/options to the >> developer. If references are opt-in anyway, I see that as accepting the >> trade-offs. If a developer doesn't want to allow by-ref modifications >> of the property, why would they make it referenceable in the first >> place? This sounds a bit like disallowing regular public properties >> because they might be modified outside the class - that's kind of the >> point, surely. >> >>> == Re >>> >>> == Re arrays >>> >>>> Regarding arrays, have you considered allowing array-index writes if >>> an &get hook is defined? i.e. "$x->foo['bar'] = 42;" could >>> be treated >>> as semantically equivalent to "$_temp =& $x->foo; $_temp['bar'] = >>> 42; >>> unset($_temp);" >>> >>> That's already discussed in the RFC: >>> >>>> The simplest approach would be to copy the array, modify it accordingly, and pass >>>> it to set hook. This would have a large and obscure performance penalty, and a set implementation >>>> would have no way of knowing which values had changed, leading to, for instance, code needing to >>>> revalidate all elements even if only one has changed. >>> >>> Unless we were OK with that bypassing the set hook entirely if defined, which, as noted >>> above, means any safety guarantees provided by a set hook are bypassed, leading to untrustworthy >>> code. >>> >>> == Re hook shorthands and return values >>> >>> Ilija and I have been discussing this for a bit, and we've both budged a little. >>> :-) Here's our counter-proposal: > > *snip* > >> I think the examples given are clear, and the lack of the top-level >> short closure-esque version makes it more obvious. Forgive me, I must >> have missed some of the previous comments - is there a reason the >> 'full' setter can't return a value, for the sake of consistency? I >> understand that you don't want "return to set" to be the *only* option, >> for the sake of e.g. change/audit logging type functionality (i.e. set >> and then some action to record that the change was made), but it seems >> a little odd and inconsistent to me that the return value of a short >> closure would be used when the return value of the long version isn't. >> This isn't really a major issue, I'm just curious if there was some >> explanation about it? > > Mainly because it introduces a lot more complexity. As far as I'm aware, determining at > runtime whether or not to make use of the return value is impossible. (Ie, we cannot differentiate > between "return", "no return statement", and "return null".) So it > would require compile time branching to generate two different pathways after lexically detecting if > there is a "return" token present somewhere in the hook body. That is probably doable, > technically, but introduces more complexity to an already necessarily-large RFC. It's also > something that is simple enough to add later as an option without any BC breaks or implications for > other parts of the design, so it's safe to punt. (Some things are harder to punt on than > others, as noted, but this one is easy/safe to skip for now.) > Right, i hadn't considered that whole "everything implicitly returns, even if it's null" scenario. Makes sense. The inconsistency is still a bit jarring but I understand the reasoning now, thanks. >>> == Re the $value variable in set > > *snip* > >>> So what makes the most sense to me is to keep $value optional, but IF you specify an >>> alternate name, you must also specify a type (which may be wider). So these are equivalent: >>> >>> public int $foo { set (int $value) => $value + 1 } >>> public int $foo { set => $value + 1 } >>> >>> And only those forms are legal. But you could also do this, if the situation called >>> for it: >>> >>> public int $foo { set(int|float $num) => floor($num) + 1; } >>> >>> This "all or nothing" approach seems like it strikes the best balance, gives >>> the most flexibility where needed while still having the least redundancy when not needed, and when >>> a name/type is provided, its behavior is the same as for a method being inherited. >>> >>> Does that sound acceptable? (Again, question for everyone.) >>> >> >> My only question with this is the same as I had in an earlier reply >> (and I'm not sure it was ever answered directly?), and you allude to >> this yourself: everywhere *else*, ($var) means a parameter >> with type >> mixed. Why is the type *required* here, when you've >> specifically said >> you want to avoid boilerplate? If we're going to assume people can >> understand that `(implicit property-type $value) is implicit, surely we >> can also assume that they will understand "specifying a parameter >> without a type" means the parameter has no type (i.e. is >> mixed). >> >> Again, for myself I'd be likely to type it (or regular parameters, >> properties, etc) as mixed if that's what I want >> *anyway*, but the >> inconsistency here seems odd, unless there's some until-now unknown >> drive to deprecate type-less parameters/properties/etc. > > If we went this route, then an untyped set param would likely imply "mixed", just > like on methods. Which, since mixed is the super type of everything, would still technically work, > but would weaken the type enforcement and thus static analysis potential. (Just as a method param > can be widened in a child class all the way to mixed/omitted, and it would be unwise for all the > same reasons.) > Having a mixed param in the set hook shouldn't weaken the actual backing parameter though - when the hook writes to $this->prop, the parent type is still enforced, *surely*? If not, why not? As for how static analysis tools handle this concept - I'd have thought it's too early to suggest what static analysis tools will or won't support given how much they already support based on less-formal syntax like docblocks. It's *already* possible to have a property that is reported (to static analysis tools/IDEs/etc) as a fixed type, but accepts a wider type on write, with the current mish-mash of typed properties, docblocks, magic getters and setters, and the bizarre unset behaviour. This is simply converting that into standardised syntax. The RFC itself proposes a scenario where a wider type is accepted in the set hook. I find it hard to believe that a static analysis tool can model "this property is Foo but it accepts string|Foo on write" but not "this property is Foo but it accepts mixed on write". Heck if that's such a problem what is said tool going to do when someone *explicitly* widens the parameter to mixed in a set hook? I would argue that if the language is providing better support for typed properties by adding 'hooks' like this, the need for static analysis of those specific parts reduces greatly - if someone wants to accept mixed when storing as a string, and convert it in the hook, and the language can **enforce those types at runtime**, why should some hypothetical static analysis be a hangup for that? > In the RFC as currently written, omitted means "derive from the property," which is a > concept that doesn't exist in methods; the closest equivalent would be if omitting a type in a > child method parameter meant "use the parent's type implicitly," which is not how > that works right now. For the third time: I'm well aware of how parameter types work everywhere else, and that's why I'm asking why the same behaviour isn't being followed here? - You've said you want to avoid boilerplate; and - You've said you would expect most people to just use the implicit same-type $value parameter; and - You've acknowledged that the existing 'standard' is that a parameter without a type is considered mixed; and - You've acknowledged in your RFC that there is a use-case for wanting to accept a wider type than what a property stores internally. So why then is it so unacceptable that the existing standard be followed, such that a set hook with an "untyped" parameter would be treated as mixed as it is everywhere else? Yes, I know you said "widening to mixed is unwise". I don't seem to recall amongst all the type-related previous RFCs, any that suggested that child parameters widening to mixed (either explicitly or implicitly) should be deprecated, so I'm sorry but I don't see much value in that argument. > > --Larry Garfield > Cheers Stephen

« previous php.internals (#122537) next »