Hi Rowan

On Sat, Mar 16, 2024 at 9:32 AM Rowan Tommins [IMSoP]
<imsop....@rwec.co.uk> wrote:
>
> On 16 March 2024 00:19:57 GMT, Larry Garfield <la...@garfieldtech.com> wrote:
>
> >Well, reading/writing from within a set/get hook is an obvious use case to 
> >support.  We cannot do cached properties easily otherwise:
> >
> >public string $expensive {
> >  get => $this->expensive ??= $this->compute();
> >  set {
> >    if (strlen($value) < 50) throw new Exception();
> >    $this->expensive = $value;
> >  }
> >}
>
>
> To play devil's advocate, in an implementation with only virtual properties, 
> this is still perfectly possible, just one declaration longer:
>
> private string $_expensive;
> public string $expensive {
>   get => $this->_expensive ??= $this->compute();
>   set {
>     if (strlen($value) < 50) throw new Exception();
>     $this->_expensive = $value;
>   }
> }
>
> Note that in this version there is an unambiguous way to refer to the raw 
> value from anywhere else in the class, if you wanted a clearAll() method for 
> instance.
>
> I can't stress enough that this is where a lot of my thinking comes from: 
> that backed properties are really the special case, not the default. Anything 
> you can do with a backed property you can do with a virtual one, but the 
> opposite will never be true.
>
>
> The minimum version of backed properties is basically just sugar for that - 
> the property is still essentially virtual, but the language declares the 
> backing property for you, leading to:
>
> public string $expensive {
>   get => $field ??= $this->compute();
>   set {
>     if (strlen($value) < 50) throw new Exception();
>     $field = $value;
>   }
> }
>
> I realise now that this isn't actually how the current implementation works, 
> but again I wanted to illustrate where I'm coming from: that backed 
> properties are just a convenience, not a different type of property with its 
> own rules.

That's not really how we think about it. Our design decisions have
been guided by a few factors:

1. The RFC intentionally makes plain properties and properties with
hooks as fully compatible as possible.

A subclass can override a plain property by adding hooks to it. Many
other languages only allow doing so if the parent property already has
generated accessors (`{ get; set; }`). For many of them, switching
from a plain property to one with accessors is actually an ABI break.
One requires generating assembly/IR instructions that access a field
in some structure, the other one is a method call. This is not
relevant in our case.

In most languages, a consequence of `{ get; set; }` is that such
properties cannot be passed by reference. This part _is_ relevant to
PHP, because PHP makes heavy use of explicit by-reference passing for
arrays, but not much else. However, as outlined in the RFC, arrays are
not a good use-case for hooks to begin with. So instead of fragmenting
the entirety of all PHP code bases into plain and `{ get; set; }`
properties where it doesn't actually make a semantic difference, and
then not even using them when it would matter (arrays), we have
decided to avoid generated hooks altogether.

The approach of making plain and hooked properties compatible also
immediately means that a property can have both a "backing value"
(inherited from the parent property) and hooks (from the child
property). This goes against your model that backed properties are
really just two properties, one for the backing value and a virtual
one for the hooks.

Our approach has the nice side effect of properties only containing
hooks when they actually do something. We don't need to deal with
optimizations like "the hook is auto-generated, revert to accessing
the property directly to make it faster", or even just having the
generated hook taking up unnecessary memory. You can think of our
properties this way:

```php
class Property {
    public ?Data $storage;
    public ?callable $getHook;
    public ?callable $setHook;

    public function get() {
        if ($hook = $this->getHook) {
            return $hook();
        } else if ($storage) {
            return $storage->get();
        } else {
            throw new Error('Property is write-only');
        }
    }

    public function set($value) {
        if ($hook = $this->setHook) {
            $hook($value);
        } else if ($storage) {
            $storage->set($value);
        } else {
            throw new Error('Property is read-only');
        }
    }
}
```

Properties can inherit both storage and hooks from their parent.
Hopefully, that helps with the mental model. Of course, in reality it
is a bit more complicated due to guards and references.

2. Although you say backed properties are just syntactic, they really
are not. For example, renaming a public property, making it private
and replacing it with a new passthrough virtual property breaks
serialization, as serialization works on the object's raw values. On
the other hand, adding a hook to an existing property doesn't
influence its backing value, so there is no impact on serialization.

> > Being the same also makes the language more predictable, which is also a 
> > design goal for this RFC.  (Hence why "this is the same logic as 
> > methods/__get/other very similar thing" is mentioned several times in the 
> > RFC.  Consistency in expectations is generally a good thing.)
>
> I can only speak for myself, but my expectations were based on:
>
> a) How __get and __set are used in practice. That generally involves reading 
> and writing a private property, of either the same or different name from the 
> public one; and that private property is visible everywhere equally, no 
> special handling based on the call stack.
>
> b) What happens if you accidentally cause infinite recursion in a normal 
> function or method, which is that the language eventually hits a stack depth 
> limit and throws an error.
>
> So the assertion that the proposal was consistent with expectations surprised 
> me. It feels to me like something that will seem surprising to people when 
> they first encounter it, but useful once they understand the implications.

Guards are used for dynamic property creation within `__get`/`__set`:
https://3v4l.org/6u3SR#v8.3.4

When `__get` or `__set` are called, the object remembers that this
property is being accessed via magic method. When you're already
inside this magic method, another call will not be triggered, thus
falling back to accessing the actual property of the object. In this
case, this means adding a dynamic property.

Dynamic properties are not particularly relevant today. The point was
not to show how similar these two cases are, but to explain that
there's an existing mechanism in place that works very well for hooks.
We may invent some new mechanism to access the backing value, like
`field = 'value'`, but for what reason? This would only make sense if
the syntax we use is useful for something else. However, given that
without guards it just leads to recursion, which I really can't see
any use for, I don't see the point.

Ilija

Reply via email to