Re: RFC - Immutable classes

From: Date: Tue, 06 Sep 2016 16:01:39 +0000
Subject: Re: RFC - Immutable classes
References: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  Groups: php.internals 
Request: Send a blank email to internals+get-95699@lists.php.net to get a copy of this message
On 09/05/2016 11:37 AM, Fleshgrinder wrote:
On 9/5/2016 10:26 AM, Michał Brzuchalski wrote:
I had a talk at Room11 and we discussed idea of mutator keyword. There were some concerns using mutator as a keyword - that's because immutable object is not being muted and also magically appeared $clone would be confusing. There's an idea of creating clone before function begins and operating simply on $this while it's newly created clone from immutable object instance and the additional keyword for such method would be for eg. transformer, so basically it may look like this: immutable class Money {
     public $amount = 0;
     public $currency;
     public function __construct($amount, $currency) {
         $this->amount = $amount;
         $this->currency = $currency;
     }
     public transformer function amount($newamount) {
         $this->amount = $newAmount; // $this actually is newly created clone
         return $this;
     }
} $oneHundret = new Money(100, $eur); $twoHundret = $oneHundret->amount(200); How about that?
The thing about mutator is only partly true because setters are generally just called mutator, it does not state anything about how it mutates something. Note that the keyword might proof useful in nonimmutable classes too for other use cases in the engine. Hence, I would not throw it off board just yet.
Naming things is hard. :-) While working on PSR-13 for FIG, we ended up with the name "evolvable" for interfaces with the withFoo() methods. I am not necessarily endorsing that as the best term (we weren't super happy about it but it was better than the alternatives), but providing it as a data point.
It is true that $clone might come as a surprise and that's why I proposed to pass it as the first argument to the mutator function. However, you were right that that is a suboptimal proposal (and it pretty much goes completely against my previous paragraph). However, always providing $this as a clone is also not a good idea because you might want to return the same instance. This really depends on the kind of action that is desired. It's probably much simpler to keep the clone requirement and unseal the clone while making the __clone method protected by default. immutable prototype {
     protected function __clone();
} immutable class Money {
     public $amount;
     public $currency;
     public function __construct(int $amount, Currency $currency) {
       $this->amount = $amount;
       $this->currency = $currency;
     }
     public function withAmount(int $amount) {
       if ($this->amount === $amount) {
         return $this;
       }
       $clone = clone $this;
       $clone->amount = $amount;
       return $clone;
     }
} This might seem like we haven't achieved much compared to the current state of affairs but we actually did: 1. No introduction of additional keywords (besides immutable) 2. No surprises (magic $clone variable) 3. No hard limitation on cloning (but disallowed by default) * 4. Full freedom to developers when to clone 5. Full freedom to developers what to return * Simply because a hard limitation is not required. Let people clone the immutable instances if they want too. Nothing bad happens besides wasting performance. I think that this is the simplest and thus best solution. :)
How big of a need is it to allow returning $this instead of $clone, and/or can that be internalized somehow as well? With copy-on-write, is that really an issue beyond a micro-optimization? As an end-user/developer, it's unclear to me how I'd know visually what scopes an object can be modified in. Basically, in the above example there's an implicit unlock-on-clone and lock-on-return. There's no clear indication of that, however, since there's no extra keywords. Is that sufficiently obvious for developers? I fear not. Also, would the above still allow for custom clone() implementations on an immutable object or no? I' not sure off hand which I'd prefer, honestly... Another note: This would preclude "externally immutable" objects, eg, ones that can compute and internally cache a value but are still effectively immutable as the outside world sees them. That's probably acceptable since the manual way is still available, but I thought it worth calling out. I definitely like any of these options better than an explicit user-facing lock/unlock mechanism, as that's begging for abuse, confusion, and inconsistency. --Larry Garfield

« previous php.internals (#95699) next »