On Thu, Jul 10, 2025, at 17:34, Larry Garfield wrote:
> On Thu, Jul 10, 2025, at 5:43 AM, Tim Düsterhus wrote:
> > Hi
> >
> > Am 2025-07-08 17:32, schrieb Nicolas Grekas:
> >> I also read Tim's argument that new features could be stricter. If one
> >> wants to be stricter and forbid extra behaviors that could be added by
> >> either the proposed hooks or __get, then the answer is : make the class
> >> final. This is the only real way to enforce readonly-ness in PHP.
> >
> > Making the class final still would not allow to optimize based on the 
> > fact that the identity of a value stored in a readonly property will not 
> > change after successfully reading from the property once. Whether or not 
> > a property hooked must be considered an implementation detail, since a 
> > main point of the property hooks RFC was that hooks can be added and 
> > removed without breaking compatibility for the user of the API.
> >
> >> engine-assisted strictness in this case. You cannot write such code in 
> >> a
> >> non-readonly way by mistake, so it has to be by intent.
> >
> > That is saying "it's impossible to introduce bugs".
> >
> >> PS: as I keep repeating, readonly doesn't immutable at all. I know this 
> >> is
> >> written as such in the original RFC, but the concrete definition and
> >> implementation of readonly isn't: you can set mutable objects to 
> >> readonly
> >> properties, and that means even readonly classes/properties are 
> >> mutable, in
> >> the generic case.
> >
> > `readonly` guarantees the immutability of identity. While you can 
> > certainly mutate mutable objects, the identity of the stored object 
> > doesn't change.
> >
> > Best regards
> > Tim Düsterhus
> 
> Nick previously suggested having the get-hook's first return value cached; it 
> would still be subsequently called, so any side effects would still happen 
> (though I don't know why you'd want side effects), but only the first 
> returned value would ever get returned.  Would anyone find that acceptable?  
> (In the typical case, it would be the same as the current $this->foo ??= 
> compute() pattern, just with an extra cache entry.)
> 
> --Larry Garfield
> 

I think that only covers one use-case for getters on readonly classes. Take 
this example for discussion:

readonly class User {
    public int $elapsedTimeSinceCreation { get => time() - $this->createdAt; }
    private int $cachedResult;
    public int $totalBalance { get => $this->cachedResult ??= 5+10; }
    public int $accessLevel { get => getCurrentAccessLevel(); }
    public function __construct(public int $createdAt) {}
}

$user = new User(time() - 5);
var_dump($user->elapsedTimeSinceCreation); // 5
var_dump($user->totalBalance); // 15
var_dump($user->accessLevel); // 42

In this example, we have three of the most common ones:
 1. Computed Properties (elapsedTimeSinceCreation): these are properties of the 
object that are relevant to the object in question, but are not static. In this 
case, you are not writing to the object. It is still "readonly".
 2. Memoization (expensiveCalculation): only calculate the property once and 
only once. This is a performance optimization. It is still "readonly".
 3. External State (accessLevel): properties of the object that rely on some 
external state, which due to architecture or other convienence may not make 
sense as part of object construction. It is still "readonly".
You can mix-and-match these to provide your own level of immutability, but 
memoization is certainly not the only one. 

You could make the argument that these should be functions, but I'd posit that 
these are properties of the user object. In other words, a function to get 
these values would probably be named `getElapsedTimeSinceCreation()`, 
`getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway.

— Rob

Reply via email to