On Sat, Nov 23, 2024, at 18:34, Rowan Tommins [IMSoP] wrote: > On 23/11/2024 16:05, Rob Landers wrote: >>> Your RFC doesn't discuss this - the changeName example shows behaviour >>> *inside* the method, but not behaviour when *calling* it >> >> An interesting observation, can you explain more as to what you mean? > > Looking closer, there's a hint at what you expect to happen in your Rectangle > example: > > > > $bigRectangle = $rectangle->resize(10, 20); > assert($bigRectangle !== $rectangle); // true > > > It seems that modifications to $this aren't visible outside the method, > creating a purely local clone, which would be discarded if it wasn't returned > (or saved somewhere). > > I can see the logic, but the result is a bit unintuitive: > > > > data class Example { > public function __construct(public int $x) {} > public function inc(): void { > $this->x++; > } > } > $foo = new Example(0); > $foo->x++; > $foo->inc(); > echo $foo->x; // 1, not 2 > >
Interesting! I actually found it to be intuitive. Think of it like this: function increment(array $array) { $array[0]++; } $arr = [0]; increment($arr); echo $arr[0]; // is 0 We don't expect $arr to be any different outside of the function because $arr is a value, not a reference. "data classes" are "values" and not references to values, thus when you modify $this, you modify the value, and it doesn't affect values elsewhere. If you want to keep track of that value, you have to put it somewhere where you can reference it—a return value, global variable, property in a regular class, etc. In any case, lets keep going to see if there is a better way. > I think it would be clearer to prevent direct modification of $this: > > > > data class Example { > public function __construct(public int $x) {} > public function inc(): void { > $this->x++; // ERROR: Can not mutate $this in data class > } > public function withInc(): static { > $new = $this; // explicitly make a local copy of $this > $new->x++; // copy-on-write separates $new from $this > return $new; > } > } > > Not that I disagree (see the records RFC), but at that point, why not make data classes implicitly readonly? > > > That would still be compatible with Ilija's suggestion, which was to add > special "mutating methods": > > > > > data class Example { > public function __construct(public int $x) {} > public mutating function inc(): void { > $this->x++; > } > } > $foo = new Example(0); > $foo->x++; > $foo->inc!(); // copy-on-write triggered *before* the method is called > echo $foo->x; // 2 > > I actually find this appealing, but it is strange to me to allow this syntax on classes. Is there precedent for that? Or is there a way we can do it using "regular looking PHP"; or are structs the way to go? Another alternative would be that mutations still trigger a copy-on-write, but the outer variable is updated with $this upon return. So this would work: data class Example { public function __construct(public int $x) {} public function inc(): void { $this->x++; } } $foo = new Example(1); $bar = $foo; $foo->inc(); // foo is copied on mutation, and $foo points at the new value on return. echo $bar->x; // 1 echo $foo->x; // 2 To me, this seems like it would be even more intuitive; $foo has the value you would expect from outside the class and doesn't require you keeping track of the value yourself. Though, there are some footguns here too: class Foo { Example $bar; function baz() { $bar = $this->bar; // should be $bar = &$this->bar $bar->inc(); echo $this->bar->x; // not incremented } } I could go either way on this one, honestly; so it makes sense to me why structs would have a dedicated syntax for whichever you prefer. On the one hand, it currently requires you to be explicit all the time (which can be annoying), and on the other hand, there's the implicit copy here, which requires you to be explicit when you want a reference. I suppose there is a third option as well, and that is to not to any of these options and just have data classes that always compare by value. > ??-- > Rowan Tommins > [IMSoP] — Rob