Analysis of property visibility, immutability, and cloning proposals

php.internals

Larry Garfield

5 years ago
There's been a number of discussions of late around property visibility and how to make objects more immutable. Since it seems to have been well-received in the past, I decided to do a complete analysis and context of the various things that have been floated about recently. The full writeup is here: https://peakd.com/hive-168588/@crell/object-properties-and-immutability I hope it proves stimulating, at least of discussion and not naps.
-- Larry Garfield larry@garfieldtech.com

Marc

5 years ago
On 28.12.20 21:23, Larry Garfield wrote:
> There's been a number of discussions of late around property visibility and how to make objects more immutable. Since it seems to have been well-received in the past, I decided to do a complete analysis and context of the various things that have been floated about recently. > > The full writeup is here: > > https://peakd.com/hive-168588/@crell/object-properties-and-immutability > > I hope it proves stimulating, at least of discussion and not naps. >
Thanks for the nice write up Larry! Is there a reason you didn't mention the proposal for immutable classes? (probably because it never went into a final RFC) https://externals.io/message/94913#94913 https://externals.io/message/79180#79180

Olle Härstedt

5 years ago
2020-12-29 8:26 GMT, Marc <marc@mabe.berlin>:
> > On 28.12.20 21:23, Larry Garfield wrote: >> There's been a number of discussions of late around property visibility >> and how to make objects more immutable. Since it seems to have been >> well-received in the past, I decided to do a complete analysis and context >> of the various things that have been floated about recently. >> >> The full writeup is here: >> >> https://peakd.com/hive-168588/@crell/object-properties-and-immutability >> >> I hope it proves stimulating, at least of discussion and not naps. >> > > Thanks for the nice write up Larry! > > Is there a reason you didn't mention the proposal for immutable classes? > (probably because it never went into a final RFC) > > https://externals.io/message/94913#94913 > > https://externals.io/message/79180#79180 > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php > >
I just want to mention that immutability might be applied too liberally in the current discourse, and in some cases, what you really want is *non-aliasing*, that is, uniqueness, to solve problems related to immutability. I think methods like `withX` is an anti-pattern, in fact, and a symptom that you do not *really* want immutability, but rather uniqueness, at least in some cases. Olle

Rowan Collins

5 years ago
On 29/12/2020 10:28, Olle Härstedt wrote:
> I just want to mention that immutability might be applied too > liberally in the current discourse, and in some cases, what you really > want is*non-aliasing*, that is, uniqueness, to solve problems related > to immutability. I think methods like `withX` is an anti-pattern, in > fact, and a symptom that you do not*really* want immutability, but > rather uniqueness, at least in some cases.
Hi Olle, I'm afraid I don't follow what you mean by "non-aliasing" and "uniqueness" here. Could you clarify, perhaps with some examples? Cheers,
-- Rowan Tommins [IMSoP]

Olle Härstedt

5 years ago
2020-12-29 21:36 GMT, Rowan Tommins <rowan.collins@gmail.com>:
> On 29/12/2020 10:28, Olle Härstedt wrote: >> I just want to mention that immutability might be applied too >> liberally in the current discourse, and in some cases, what you really >> want is*non-aliasing*, that is, uniqueness, to solve problems related >> to immutability. I think methods like `withX` is an anti-pattern, in >> fact, and a symptom that you do not*really* want immutability, but >> rather uniqueness, at least in some cases. > > > Hi Olle, > > I'm afraid I don't follow what you mean by "non-aliasing" and > "uniqueness" here. Could you clarify, perhaps with some examples? > > Cheers, > > -- > Rowan Tommins > [IMSoP] >
Wikipedia has an article about aliasing: https://en.wikipedia.org/wiki/Aliasing_(computing) Also relevant: https://en.wikipedia.org/wiki/Uniqueness_type Uniqueness is when you only allow _one_ reference to an object (or bucket of memory). $a = new A(); $b = $a; // Both $a and $b point to the same place in memory, so you have an alias Uniqueness and immutability solves similar problems (at least in a GC language like PHP): Spooky action at a distance, fragile composition, rep exposure. The are more advanced systems of ownership than just uniqueness, e.g. Universe Types, but let's ignore that for now. https://www.researchgate.net/publication/221321963_Ownership_transfer_in_Universe_Types Uniqueness has the benefit of being more performant than immutability, since it leads to less memory copy (but of course it's not certain this performance gain matters in PHP programs). You can compare a builder pattern with immutability vs non-aliasing (uniqueness): ``` // Immutable $b = new Builder(); $b = $b->withFoo()->withBar()->withBaz(); myfun($b); // $b is immutable, so $b cannot be modified by myfun() return $b; ``` ``` // Uniqueness $b = new Builder(); // class Builder is annotated as non-aliasing/unique $b->addFoo(); $b->addBar(); $b->addBaz(); myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. return $b; ``` The guarantee in both above snippets is that myfun() DOES NOT modify $b before returning it. BUT with immutability, you have to copy $b three times, with uniqueness only one. That's why I think ownership system merits some attention, not ONLY immutability. :) Unfortunately, it can take a looong time to force new concepts like these into common discourse. Rust helps, obviously. The language Clean has opt-in uniqueness (I need to read up more on it, tho). Linear types in e.g. Haskell is also related (but is both more complex and more powerful). I would like to add an annotation to Psalm, like @psalm-no-alias, because this is also needed to make type-state sane. But, didn't do much yet, only some brain farting. :) Olle

Rowan Collins

5 years ago
On 30/12/2020 13:49, Olle Härstedt wrote:
> Uniqueness is when you only allow _one_ reference to an object (or > bucket of memory). > [...] > > You can compare a builder pattern with immutability vs non-aliasing > (uniqueness): > > ``` > // Immutable > $b = new Builder(); > $b = $b->withFoo()->withBar()->withBaz(); > myfun($b); // $b is immutable, so $b cannot be modified by myfun() > return $b; > ``` > > ``` > // Uniqueness > $b = new Builder(); // class Builder is annotated as non-aliasing/unique > $b->addFoo(); > $b->addBar(); > $b->addBaz(); > myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. > return $b; > ```
Thanks, I can see how that solves a lot of the same problems, in a very robustly analysable way. However, from a high-level user-friendliness point of view, I think "withX" methods are actually more natural than explicitly cloning mutable objects. Consider the case of defining a range: firstly, with plain integers and familiar operators: $start = 1; $end = $start + 5; This models integers as immutable values, and + as an operator which returns a new instance. If integers were mutable but not aliasable, we would instead write something like this: $start = 1; $end = clone $start; $end += 5; // where += would be an in-place modification, not a short-hand for assignment I think the first more naturally expresses the desired algorithm. It's therefore natural to want the same for a range of dates: $start = MyDate::today(); $end = $start->withAddedDays(5); vs $start = MyDate::today(); $end = clone $start; $end->addDays(5); To put it a different way, value types naturally form *expressions*, which mutable objects model clumsily. It would be very tedious if we had to avoid accidentally mutating the speed of light: $e = (clone $m) * ((clone $c) ** 2);
> The guarantee in both above snippets is that myfun() DOES NOT modify > $b before returning it. BUT with immutability, you have to copy $b > three times, with uniqueness only one.
I wonder if that difference can be optimised out by the compiler/OpCache: detect clones that immediately replace their original, and optimise it to an in-place modification. In other words, compile $foo = clone $foo with { x: 42 } to $foo->x = 42, even if the clone is actually in a "withX" method. Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

5 years ago
On Wed, Dec 30, 2020, at 12:15 PM, Rowan Tommins wrote:
> On 30/12/2020 13:49, Olle Härstedt wrote: > > Uniqueness is when you only allow _one_ reference to an object (or > > bucket of memory). > > [...] > > > > You can compare a builder pattern with immutability vs non-aliasing > > (uniqueness): > > > > ``` > > // Immutable > > $b = new Builder(); > > $b = $b->withFoo()->withBar()->withBaz(); > > myfun($b); // $b is immutable, so $b cannot be modified by myfun() > > return $b; > > ``` > > > > ``` > > // Uniqueness > > $b = new Builder(); // class Builder is annotated as non-aliasing/unique > > $b->addFoo(); > > $b->addBar(); > > $b->addBaz(); > > myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. > > return $b; > > ``` > > > Thanks, I can see how that solves a lot of the same problems, in a very > robustly analysable way. > > However, from a high-level user-friendliness point of view, I think > "withX" methods are actually more natural than explicitly cloning > mutable objects. > > Consider the case of defining a range: firstly, with plain integers and > familiar operators: > > $start = 1; > $end = $start + 5; > > This models integers as immutable values, and + as an operator which > returns a new instance. If integers were mutable but not aliasable, we > would instead write something like this: > > $start = 1; > $end = clone $start; > $end += 5; // where += would be an in-place modification, not a > short-hand for assignment > > I think the first more naturally expresses the desired algorithm. It's > therefore natural to want the same for a range of dates: > > $start = MyDate::today(); > $end = $start->withAddedDays(5); > > vs > > $start = MyDate::today(); > $end = clone $start; > $end->addDays(5); > > > To put it a different way, value types naturally form *expressions*, > which mutable objects model clumsily. It would be very tedious if we had > to avoid accidentally mutating the speed of light: > > $e = (clone $m) * ((clone $c) ** 2); > > > > The guarantee in both above snippets is that myfun() DOES NOT modify > > $b before returning it. BUT with immutability, you have to copy $b > > three times, with uniqueness only one.
That's a good summary of why immutability and with-er methods (or some equivalent) are more ergonomic. Another point to remember: Because of PHP's copy-on-write behavior, full on immutability doesn't actually waste that much memory. It does use up some, but far less than you think. (Again, based on the tests MWOP ran for PSR-7 a ways back.)
> I wonder if that difference can be optimised out by the > compiler/OpCache: detect clones that immediately replace their original, > and optimise it to an in-place modification. In other words, compile > $foo = clone $foo with { x: 42 } to $foo->x = 42, even if the clone is > actually in a "withX" method.
In concept, maybe? That's well above my pay grade. :-) --Larry Garfield

Olle Härstedt

5 years ago
2020-12-30 18:31 GMT, Larry Garfield <larry@garfieldtech.com>:
> On Wed, Dec 30, 2020, at 12:15 PM, Rowan Tommins wrote: >> On 30/12/2020 13:49, Olle Härstedt wrote: >> > Uniqueness is when you only allow _one_ reference to an object (or >> > bucket of memory). >> > [...] >> > >> > You can compare a builder pattern with immutability vs non-aliasing >> > (uniqueness): >> > >> > ``` >> > // Immutable >> > $b = new Builder(); >> > $b = $b->withFoo()->withBar()->withBaz(); >> > myfun($b); // $b is immutable, so $b cannot be modified by myfun() >> > return $b; >> > ``` >> > >> > ``` >> > // Uniqueness >> > $b = new Builder(); // class Builder is annotated as >> > non-aliasing/unique >> > $b->addFoo(); >> > $b->addBar(); >> > $b->addBaz(); >> > myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. >> > return $b; >> > ``` >> >> >> Thanks, I can see how that solves a lot of the same problems, in a very >> robustly analysable way. >> >> However, from a high-level user-friendliness point of view, I think >> "withX" methods are actually more natural than explicitly cloning >> mutable objects. >> >> Consider the case of defining a range: firstly, with plain integers and >> familiar operators: >> >> $start = 1; >> $end = $start + 5; >> >> This models integers as immutable values, and + as an operator which >> returns a new instance. If integers were mutable but not aliasable, we >> would instead write something like this: >> >> $start = 1; >> $end = clone $start; >> $end += 5; // where += would be an in-place modification, not a >> short-hand for assignment >> >> I think the first more naturally expresses the desired algorithm. It's >> therefore natural to want the same for a range of dates: >> >> $start = MyDate::today(); >> $end = $start->withAddedDays(5); >> >> vs >> >> $start = MyDate::today(); >> $end = clone $start; >> $end->addDays(5); >> >> >> To put it a different way, value types naturally form *expressions*, >> which mutable objects model clumsily. It would be very tedious if we had >> to avoid accidentally mutating the speed of light: >> >> $e = (clone $m) * ((clone $c) ** 2); >> >> >> > The guarantee in both above snippets is that myfun() DOES NOT modify >> > $b before returning it. BUT with immutability, you have to copy $b >> > three times, with uniqueness only one. > > That's a good summary of why immutability and with-er methods (or some > equivalent) are more ergonomic. > > Another point to remember: Because of PHP's copy-on-write behavior, full on > immutability doesn't actually waste that much memory. It does use up some, > but far less than you think. (Again, based on the tests MWOP ran for PSR-7 > a ways back.)
I thought copy-on-write was only for arrays, not objects? Olle

Larry Garfield

5 years ago
> > That's a good summary of why immutability and with-er methods (or some > > equivalent) are more ergonomic. > > > > Another point to remember: Because of PHP's copy-on-write behavior, full on > > immutability doesn't actually waste that much memory. It does use up some, > > but far less than you think. (Again, based on the tests MWOP ran for PSR-7 > > a ways back.) > > I thought copy-on-write was only for arrays, not objects? > > Olle
Copy on write applies to all values; the caveat is that with objects, the value being copied is the handle that points to an object in memory, rather than the object itself. That means passing an object by reference can do some seriously unexpected things, which is why you basically never do so. The point here is that if you have an object with 15 internal properties, it's memory usage is 15 zvals plus one zval for the object, plus one zval for the variable that points to it. (I'm over-simplifying here. A lot.) If you pass it to a function, only the one zval for the handle is duplicated, which is the same as for an integer. If you clone the object, you don't duplicate 15+1 zvals. You duplicate just the one zval for the object itself, which reuses the existing 15 internal property entries. If in the new object you then update just the third one, PHP then duplicates just that one internal zval and modifies the new one. So you still are using only 18 zvals, not 36 zvals. (Engine people: Yes, I am *very* over-simplifying. I know.) Basically, what in most languages would require manually implementing "immutable data structures" we get for free in PHP, which is seriously sweet. The net result is that a with-er chain like this: $foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z'); is way, way less expensive than it looks, both on memory and CPU. It is more expensive than setters, but not by much. That's why I don't think the distinction between unique and immutable mentioned up-thread is that big of a deal in PHP, specifically. Yes, they're different things, but the cost of them is not all that different because of CoW, so considering them separately is not as important as it would be in a language that doesn't automatically do CoW in the background for us. (Whoever in the 90s decided to bake CoW into the engine, thank you. It's an incredibly nice foundational feature.) --Larry Garfield

Chuck Adams

5 years ago
On Wed, Dec 30, 2020 at 12:27 PM Larry Garfield <larry@garfieldtech.com> wrote:
> > If you clone the object, you don't duplicate 15+1 zvals. You duplicate just the one zval for the object itself, which reuses the existing 15 internal property entries. If in the new object you then update just the third one, PHP then duplicates just that one internal zval and modifies the new one. So you still are using only 18 zvals, not 36 zvals. (Engine people: Yes, I am *very* over-simplifying. I know.) >
I've pondered hacking in something like perl's bless() to turn arrays into value objects, but according to this it looks like an object with clone-on-write behavior would be better, as I'm assuming arrays do a full shallow copy: given an array of 15 entries, pass it to a function, change one member, now you're using 15 more zvals, as opposed to just one with an object. Am I reading that right? --c

Olle Härstedt

5 years ago
On Wed, 30 Dec 2020, 20:27 Larry Garfield, <larry@garfieldtech.com> wrote:
> > > > That's a good summary of why immutability and with-er methods (or some > > > equivalent) are more ergonomic. > > > > > > Another point to remember: Because of PHP's copy-on-write behavior, > full on > > > immutability doesn't actually waste that much memory. It does use up > some, > > > but far less than you think. (Again, based on the tests MWOP ran for > PSR-7 > > > a ways back.) > > > > I thought copy-on-write was only for arrays, not objects? > > > > Olle > > Copy on write applies to all values; the caveat is that with objects, the > value being copied is the handle that points to an object in memory, rather > than the object itself. That means passing an object by reference can do > some seriously unexpected things, which is why you basically never do so. > > The point here is that if you have an object with 15 internal properties, > it's memory usage is 15 zvals plus one zval for the object, plus one zval > for the variable that points to it. (I'm over-simplifying here. A lot.) > If you pass it to a function, only the one zval for the handle is > duplicated, which is the same as for an integer. > > If you clone the object, you don't duplicate 15+1 zvals. You duplicate > just the one zval for the object itself, which reuses the existing 15 > internal property entries. If in the new object you then update just the > third one, PHP then duplicates just that one internal zval and modifies the > new one. So you still are using only 18 zvals, not 36 zvals. (Engine > people: Yes, I am *very* over-simplifying. I know.) > > Basically, what in most languages would require manually implementing > "immutable data structures" we get for free in PHP, which is seriously > sweet. > > The net result is that a with-er chain like this: > > $foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z'); > > is way, way less expensive than it looks, both on memory and CPU. It is > more expensive than setters, but not by much. >
Ok. You have a benchmark for this? I can make one otherwise, for the query example. It worries me a little that immutablility is pushed into the ecosystem as a silver bullet. Main reason functional languages are using it is because ownership is a newer concept, so it hasn't been adapted as much.

Olle Härstedt

5 years ago
2020-12-30 18:15 GMT, Rowan Tommins <rowan.collins@gmail.com>:
> On 30/12/2020 13:49, Olle Härstedt wrote: >> Uniqueness is when you only allow _one_ reference to an object (or >> bucket of memory). >> [...] >> >> You can compare a builder pattern with immutability vs non-aliasing >> (uniqueness): >> >> ``` >> // Immutable >> $b = new Builder(); >> $b = $b->withFoo()->withBar()->withBaz(); >> myfun($b); // $b is immutable, so $b cannot be modified by myfun() >> return $b; >> ``` >> >> ``` >> // Uniqueness >> $b = new Builder(); // class Builder is annotated as non-aliasing/unique >> $b->addFoo(); >> $b->addBar(); >> $b->addBaz(); >> myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. >> return $b; >> ``` > > > Thanks, I can see how that solves a lot of the same problems, in a very > robustly analysable way. > > However, from a high-level user-friendliness point of view, I think > "withX" methods are actually more natural than explicitly cloning > mutable objects. > > Consider the case of defining a range: firstly, with plain integers and > familiar operators: > > $start = 1; > $end = $start + 5; > > This models integers as immutable values, and + as an operator which > returns a new instance. If integers were mutable but not aliasable, we > would instead write something like this: > > $start = 1; > $end = clone $start; > $end += 5; // where += would be an in-place modification, not a > short-hand for assignment > > I think the first more naturally expresses the desired algorithm. It's > therefore natural to want the same for a range of dates: > > $start = MyDate::today(); > $end = $start->withAddedDays(5); > > vs > > $start = MyDate::today(); > $end = clone $start; > $end->addDays(5);
Sure, this is a good use-case for immutability. :)
> > > To put it a different way, value types naturally form *expressions*, > which mutable objects model clumsily. It would be very tedious if we had > to avoid accidentally mutating the speed of light: > > $e = (clone $m) * ((clone $c) ** 2);
Using a variable on right-hand side does not automatically create an alias, so in the above case you don't have to use clone. A more motivating example for uniqueness is perhaps a query builder. ``` $query = (new Query()) ->select(1) ->from('foo') ->where(...) ->orderBy(..) ->limit(); doSomething($query); doSomethingElse($query); ``` In the above snippet, we don't know if doSomething() will change $query and cause a bug. The issue can be solved with an immutable builder, using withSelect(), withWhere(), etc, OR it's solved with uniqueness, forcing a clone to avoid creating a new alias (passing $query to a function creates an alias inside that function). The optimisation is the same as in my previous example, avoiding copying $query multiple times during build-up.
> > >> The guarantee in both above snippets is that myfun() DOES NOT modify >> $b before returning it. BUT with immutability, you have to copy $b >> three times, with uniqueness only one. > > > I wonder if that difference can be optimised out by the > compiler/OpCache: detect clones that immediately replace their original, > and optimise it to an in-place modification. In other words, compile > $foo = clone $foo with { x: 42 } to $foo->x = 42, even if the clone is > actually in a "withX" method.
I guess OCaml/Haskell does stuff like this, since everything is immutable by default there. Let's ask them? Unless someone here already knows? :) Olle

Larry Garfield

5 years ago
On Wed, Dec 30, 2020, at 12:42 PM, Olle Härstedt wrote:
> A more motivating example for uniqueness is perhaps a query builder. > > ``` > $query = (new Query()) > ->select(1) > ->from('foo') > ->where(...) > ->orderBy(..) > ->limit(); > doSomething($query); > doSomethingElse($query); > ``` > > In the above snippet, we don't know if doSomething() will change > $query and cause a bug. The issue can be solved with an immutable > builder, using withSelect(), withWhere(), etc, OR it's solved with > uniqueness, forcing a clone to avoid creating a new alias (passing > $query to a function creates an alias inside that function). The > optimisation is the same as in my previous example, avoiding copying > $query multiple times during build-up.
For a query builder, I probably wouldn't make it immutable anyway, myself. If you really want to force that doSomething() cannot modify the object that is otherwise mutable, calling doSomething(clone $query) already works today and gets that net effect, provided that Query is safe to clone. (Vis, has no service dependencies, and if it has any dependent value objects then it has a __clone() method that deep clones.)
> >> The guarantee in both above snippets is that myfun() DOES NOT modify > >> $b before returning it. BUT with immutability, you have to copy $b > >> three times, with uniqueness only one.
Yes, but with CoW those 3 copies are not that expensive, so we can most of the time ignore them except as a very micro-optimization. (See previous email.) --Larry Garfield

Mike Schinkel

5 years ago
> On Dec 30, 2020, at 1:15 PM, Rowan Tommins <rowan.collins@gmail.com> wrote: > > On 30/12/2020 13:49, Olle Härstedt wrote: >> Uniqueness is when you only allow _one_ reference to an object (or >> bucket of memory). >> [...] >> >> You can compare a builder pattern with immutability vs non-aliasing >> (uniqueness): >> >> ``` >> // Immutable >> $b = new Builder(); >> $b = $b->withFoo()->withBar()->withBaz(); >> myfun($b); // $b is immutable, so $b cannot be modified by myfun() >> return $b; >> ``` >> >> ``` >> // Uniqueness >> $b = new Builder(); // class Builder is annotated as non-aliasing/unique >> $b->addFoo(); >> $b->addBar(); >> $b->addBaz(); >> myfun(clone $b); // HAVE TO CLONE TO NOT THROW EXCEPTION. >> return $b; >> ``` > > > Thanks, I can see how that solves a lot of the same problems, in a very robustly analysable way. > > However, from a high-level user-friendliness point of view, I think "withX" methods are actually more natural than explicitly cloning mutable objects.
"User-friendliness" of this nature is in the eye of the beholder. A different perspective is that "withX" methods require a mental translation where "addX" methods do not, much like how a person whose native language is English will find it a challenge to (or cannot) "think" in French.
> Consider the case of defining a range: firstly, with plain integers and familiar operators: > > $start = 1; > $end = $start + 5; > > This models integers as immutable values, and + as an operator which returns a new instance. If integers were mutable but not aliasable, we would instead write something like this: > > $start = 1; > $end = clone $start; > $end += 5; // where += would be an in-place modification, not a short-hand for assignment > > I think the first more naturally expresses the desired algorithm. It's therefore natural to want the same for a range of dates: > > $start = MyDate::today(); > $end = $start->withAddedDays(5); > > vs > > $start = MyDate::today(); > $end = clone $start; > $end->addDays(5);
Ignoring that you are comparing apples and oranges (scalars to objects,) the latter is easier to reason about IMO.
> To put it a different way, value types naturally form *expressions*, which mutable objects model clumsily. It would be very tedious if we had to avoid accidentally mutating the speed of light: > > $e = (clone $m) * ((clone $c) ** 2); > > >> The guarantee in both above snippets is that myfun() DOES NOT modify >> $b before returning it. BUT with immutability, you have to copy $b >> three times, with uniqueness only one. > > > I wonder if that difference can be optimised out by the compiler/OpCache: detect clones that immediately replace their original, and optimise it to an in-place modification. In other words, compile $foo = clone $foo with { x: 42 } to $foo->x = 42, even if the clone is actually in a "withX" method.
-Mike

Rowan Collins

5 years ago
Hi Mike and Olle, On 31/12/2020 00:24, Mike Schinkel wrote:
> A different perspective is that "withX" methods require a mental translation where "addX" methods do not, much like how a person whose native language is English will find it a challenge to (or cannot) "think" in French.
I wonder if that's just about the choice of names, rather than the mutability/immutability itself?
>> $start = MyDate::today(); >> $end = $start->withAddedDays(5); >> >> vs >> >> $start = MyDate::today(); >> $end = clone $start; >> $end->addDays(5); > Ignoring that you are comparing apples and oranges (scalars to objects,) the latter is easier to reason about IMO.
Ignoring the distinction between "scalar" and "object" was kind of the point: they are both "values", and are more naturally treated the same as differently. To take a different example, consider writing a new number type (for arbitrary precision, or complex numbers, or whatever), with an "add" method. The mutable version looks something like this: public function add($other) {     $this->value = $this->value + $other; } and has to be used like this: $start = new MyNumber(1); $end = clone $start; $end->add(5); The immutable version might look more like this: public function add($other) {     return clone $this with { value: $this->value + $other }; } and is used like this: $start = new MyNumber(1); $end = $start->add(5); That's much closer to the "$end = $start + 5;" we're used to. On 30/12/2020 18:42, Olle Härstedt wrote:
>> To put it a different way, value types naturally form*expressions*, >> which mutable objects model clumsily. It would be very tedious if we had >> to avoid accidentally mutating the speed of light: >> >> $e = (clone $m) * ((clone $c) ** 2); > Using a variable on right-hand side does not automatically create an > alias, so in the above case you don't have to use clone.
Whether or not the type system forced you to, you'd have to use clone if the values were implemented as mutable. Switching to methods again may make that clearer: $c = new MyNumber(299_792_458); $m = new MyNumber(10); $e = $m->multiply( $c->square() ); If multiply() and square() are mutating state, rather than returning new instances, $c is now 89875517873681764, which is going to totally mess up the universe... Regards,
-- Rowan Tommins [IMSoP]

Olle Härstedt

5 years ago
2020-12-31 12:37 GMT, Rowan Tommins <rowan.collins@gmail.com>:
> Hi Mike and Olle, > > > On 31/12/2020 00:24, Mike Schinkel wrote: >> A different perspective is that "withX" methods require a mental >> translation where "addX" methods do not, much like how a person whose >> native language is English will find it a challenge to (or cannot) "think" >> in French. > > > I wonder if that's just about the choice of names, rather than the > mutability/immutability itself? > > >>> $start = MyDate::today(); >>> $end = $start->withAddedDays(5); >>> >>> vs >>> >>> $start = MyDate::today(); >>> $end = clone $start; >>> $end->addDays(5); >> Ignoring that you are comparing apples and oranges (scalars to objects,) >> the latter is easier to reason about IMO. > > > Ignoring the distinction between "scalar" and "object" was kind of the > point: they are both "values", and are more naturally treated the same > as differently. > > > To take a different example, consider writing a new number type (for > arbitrary precision, or complex numbers, or whatever), with an "add" > method. > > The mutable version looks something like this: > > public function add($other) { > $this->value = $this->value + $other; > } > > and has to be used like this: > > $start = new MyNumber(1); > $end = clone $start; > $end->add(5); > > > The immutable version might look more like this: > > public function add($other) { > return clone $this with { value: $this->value + $other }; > } > > and is used like this: > > $start = new MyNumber(1); > $end = $start->add(5); > > That's much closer to the "$end = $start + 5;" we're used to. > > > On 30/12/2020 18:42, Olle Härstedt wrote: >>> To put it a different way, value types naturally form*expressions*, >>> which mutable objects model clumsily. It would be very tedious if we had >>> to avoid accidentally mutating the speed of light: >>> >>> $e = (clone $m) * ((clone $c) ** 2); >> Using a variable on right-hand side does not automatically create an >> alias, so in the above case you don't have to use clone. > > > Whether or not the type system forced you to, you'd have to use clone if > the values were implemented as mutable. Switching to methods again may > make that clearer: > > $c = new MyNumber(299_792_458); > $m = new MyNumber(10); > $e = $m->multiply( $c->square() ); > > If multiply() and square() are mutating state, rather than returning new > instances, $c is now 89875517873681764, which is going to totally mess > up the universe... > > > Regards, > > -- > Rowan Tommins > [IMSoP]
Yes, of course you can find use-cases where immutability is a better choice, just like I can find use-cases where (constrained) mutability is better. The point is not to replace one tool with another, but rather adding another tool to the toolbox. The web dev discourse is one-sided with regard to immutability, I think. Wish I had time to implement a PR to Psalm to show something more concrete... Again, if you only have a hammer, everything looks like a nail. :) Olle

Larry Garfield

5 years ago
On Thu, Dec 31, 2020, at 8:04 AM, Olle Härstedt wrote:
> 2020-12-31 12:37 GMT, Rowan Tommins <rowan.collins@gmail.com>:
> > On 30/12/2020 18:42, Olle Härstedt wrote: > >>> To put it a different way, value types naturally form*expressions*, > >>> which mutable objects model clumsily. It would be very tedious if we had > >>> to avoid accidentally mutating the speed of light: > >>> > >>> $e = (clone $m) * ((clone $c) ** 2); > >> Using a variable on right-hand side does not automatically create an > >> alias, so in the above case you don't have to use clone. > > > > > > Whether or not the type system forced you to, you'd have to use clone if > > the values were implemented as mutable. Switching to methods again may > > make that clearer: > > > > $c = new MyNumber(299_792_458); > > $m = new MyNumber(10); > > $e = $m->multiply( $c->square() ); > > > > If multiply() and square() are mutating state, rather than returning new > > instances, $c is now 89875517873681764, which is going to totally mess > > up the universe... > > > > > > Regards, > > > > -- > > Rowan Tommins > > [IMSoP] > > Yes, of course you can find use-cases where immutability is a better > choice, just like I can find use-cases where (constrained) mutability > is better. The point is not to replace one tool with another, but > rather adding another tool to the toolbox. The web dev discourse is > one-sided with regard to immutability, I think. Wish I had time to > implement a PR to Psalm to show something more concrete... Again, if > you only have a hammer, everything looks like a nail. :) > > Olle
> The web dev discourse is > one-sided with regard to immutability,
Yes, if you've heard any of the regular whining about PSR-7 being an immutable object you'd think it's one-sided in favor of mutability. ;-) As you say, the point here is to add tools. Right now, doing immutability in PHP in syntactically clumsy and ugly. We want to fix that, and that has to include some means of "give me a new value based on this existing value but with some difference." (aka, exactly what with-er methods do, although I agree entirely that if you have the option of less generic names, use them). So, can we get back to the original post, which is proposing specifics of the tools to make that happen? :-) (Asymmetric visibility and clone-with, specifically.) --Larry Garfield

Rowan Collins

5 years ago
On 31/12/2020 14:04, Olle Härstedt wrote:
> Yes, of course you can find use-cases where immutability is a better > choice, just like I can find use-cases where (constrained) mutability > is better. The point is not to replace one tool with another, but > rather adding another tool to the toolbox. The web dev discourse is > one-sided with regard to immutability, I think. Wish I had time to > implement a PR to Psalm to show something more concrete... Again, if > you only have a hammer, everything looks like a nail. :)
Certainly, I didn't mean to say that immutability was always the perfect choice. I think it's popular because it's an easy hammer to borrow from the fashionable Functional Programming toolbox - you can get a lot of its advantages without much support from the language, and it genuinely fits a lot of use cases encountered in high-level programming. Where ownership concepts seem to shine is where immutability is either impossible (e.g. consuming from a network stream or an event queue) or otherwise undesirable (e.g. working with large amounts of data, or tightly optimised code). I read a bit about Uniqueness Attributes in Clean [1] and it seems they are implemented there so that the *user* can treat everything as immutable, but the *compiler* can safely mutate underlying structures. So in that implementation at least, a "mutable record" would in fact be implemented with the equivalent of "clone ... with", so that it appeared *from the outside* to return a new instance each time. It's certainly an interesting concept, particularly for the I/O case (where immutability is genuinely not an option) but how easy it would be to retro-fit to a dynamic language like PHP I'm not sure. [1] https://cloogle.org/doc/#_9 Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

5 years ago
On Tue, Dec 29, 2020, at 2:26 AM, Marc wrote:
> > On 28.12.20 21:23, Larry Garfield wrote: > > There's been a number of discussions of late around property visibility and how to make objects more immutable. Since it seems to have been well-received in the past, I decided to do a complete analysis and context of the various things that have been floated about recently. > > > > The full writeup is here: > > > > https://peakd.com/hive-168588/@crell/object-properties-and-immutability > > > > I hope it proves stimulating, at least of discussion and not naps. > > > > Thanks for the nice write up Larry! > > Is there a reason you didn't mention the proposal for immutable classes? > (probably because it never went into a final RFC) > > https://externals.io/message/94913#94913 > > https://externals.io/message/79180#79180
Two main reasons: 1) It's not been discussed recently (see how old the dates are on those messages), so I wasn't thinking about it. 2) An immutable class would in all practicality be the same as a class where all the properties are initonly (or writeonce, but that was already rejected). So any arguments for/against initonly apply in aggregate to an immutable class. --Larry Garfield

Olle Härstedt

5 years ago
2020-12-29 15:38 GMT, Larry Garfield <larry@garfieldtech.com>:
> On Tue, Dec 29, 2020, at 2:26 AM, Marc wrote: >> >> On 28.12.20 21:23, Larry Garfield wrote: >> > There's been a number of discussions of late around property visibility >> > and how to make objects more immutable. Since it seems to have been >> > well-received in the past, I decided to do a complete analysis and >> > context of the various things that have been floated about recently. >> > >> > The full writeup is here: >> > >> > https://peakd.com/hive-168588/@crell/object-properties-and-immutability >> > >> > I hope it proves stimulating, at least of discussion and not naps. >> > >> >> Thanks for the nice write up Larry! >> >> Is there a reason you didn't mention the proposal for immutable classes? >> (probably because it never went into a final RFC) >> >> https://externals.io/message/94913#94913 >> >> https://externals.io/message/79180#79180 > > Two main reasons: > > 1) It's not been discussed recently (see how old the dates are on those > messages), so I wasn't thinking about it. > > 2) An immutable class would in all practicality be the same as a class where > all the properties are initonly (or writeonce, but that was already > rejected). So any arguments for/against initonly apply in aggregate to an > immutable class. > > --Larry Garfield > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php > >
Instead of shoe-horning everything into the PHP object system, did anyone consider adding support for records instead, which would always be immutable, and could support the spread operator for cloning-with similar as in JavaScript or OCaml? They could be based on PHP arrays and thus be passed by value. Olle

Rowan Collins

5 years ago
On 29/12/2020 18:38, Olle Härstedt wrote:
> Instead of shoe-horning everything into the PHP object system, did > anyone consider adding support for records instead, which would always > be immutable, and could support the spread operator for cloning-with > similar as in JavaScript or OCaml? They could be based on PHP arrays > and thus be passed by value.
While we could create a brand new "record" or "struct" type, I think there are a few reasons to think it would end up *looking* more like objects than arrays: - we have an established syntax for declaring types of object (class Foo {...}), and none for declaring types of array - the 'bar' in $foo['bar'] is an expression, implying dynamic options; the bar in $foo->bar is a bare identifier, implying statically defined options - similarly, we have a syntax for creating object instances, with statically analysable members: new Foo(bar: 42) The spread operator could be made to work with either style, if we preferred it to using "clone ... with ...": - ['bar'=>69, ...$existingFoo] - new Foo(bar: 69, ...$existingFoo) However, arrays arguably already have a clone-with syntax, more normally thought of as "copy-on-write". Rather than "mutable with special logic to pass and assign by value", I think you can model their behaviour as "immutable with special logic to clone with modifications": $foo = ['bar'=>42, 'baz'=>101]; $newFoo = $foo; // lazy assignment by value is indistinguishable from assignment by pointer $newFoo['bar'] = 69; // $newFoo is a modified clone of $foo $newFoo['bar'] = 72; // mutating $newFoo in place is indistinguishable from creating and assigning another modified clone In theory, "records" could have this ability with object-like syntax: $foo = new Foo(bar: 42, baz: 101); $newFoo = $foo; $newFoo->bar = 69; // $newFoo is a modified clone $newFoo->bar = 72; // can be optimised as in-place modification, but conceptually cloning again In the simple case, that's equivalent to a clone-with: $foo = new Foo(bar: 42, baz: 101); $newFoo = clone $foo with { bar: 69 }; $newFoo = clone $newFoo with { bar: 72 }; // can probably be optimised the same way as the above examples It would allow more complex modifications, though, such as deep modification: $foo = new Foo(bar: new Bar(name: 'Bob')); $newFoo = $foo; $newFoo->bar->name = 'Robert'; That last line would do the same as this: $newFoo = clone $newFoo with { bar: clone $newFoo->bar with { name: 'Robert' }}; How desirable that is, and how it fits with the use cases in Larry's post, I'm not sure. Regards,
-- Rowan Tommins [IMSoP]

Olle Härstedt

5 years ago
2020-12-29 22:43 GMT, Rowan Tommins <rowan.collins@gmail.com>:
> On 29/12/2020 18:38, Olle Härstedt wrote: >> Instead of shoe-horning everything into the PHP object system, did >> anyone consider adding support for records instead, which would always >> be immutable, and could support the spread operator for cloning-with >> similar as in JavaScript or OCaml? They could be based on PHP arrays >> and thus be passed by value. > > > While we could create a brand new "record" or "struct" type, I think > there are a few reasons to think it would end up *looking* more like > objects than arrays: > > - we have an established syntax for declaring types of object (class Foo > {...}), and none for declaring types of array > - the 'bar' in $foo['bar'] is an expression, implying dynamic options; > the bar in $foo->bar is a bare identifier, implying statically defined > options > - similarly, we have a syntax for creating object instances, with > statically analysable members: new Foo(bar: 42) > > > The spread operator could be made to work with either style, if we > preferred it to using "clone ... with ...": > > - ['bar'=>69, ...$existingFoo] > - new Foo(bar: 69, ...$existingFoo) > > > However, arrays arguably already have a clone-with syntax, more normally > thought of as "copy-on-write". Rather than "mutable with special logic > to pass and assign by value", I think you can model their behaviour as > "immutable with special logic to clone with modifications": > > $foo = ['bar'=>42, 'baz'=>101]; > $newFoo = $foo; // lazy assignment by value is indistinguishable from > assignment by pointer > $newFoo['bar'] = 69; // $newFoo is a modified clone of $foo > $newFoo['bar'] = 72; // mutating $newFoo in place is indistinguishable > from creating and assigning another modified clone > > > In theory, "records" could have this ability with object-like syntax: > > $foo = new Foo(bar: 42, baz: 101); > $newFoo = $foo; > $newFoo->bar = 69; // $newFoo is a modified clone > $newFoo->bar = 72; // can be optimised as in-place modification, but > conceptually cloning again > > > In the simple case, that's equivalent to a clone-with: > > $foo = new Foo(bar: 42, baz: 101); > $newFoo = clone $foo with { bar: 69 }; > $newFoo = clone $newFoo with { bar: 72 }; // can probably be optimised > the same way as the above examples > > > It would allow more complex modifications, though, such as deep > modification: > > $foo = new Foo(bar: new Bar(name: 'Bob')); > $newFoo = $foo; > $newFoo->bar->name = 'Robert'; > > That last line would do the same as this: > > $newFoo = clone $newFoo with { bar: clone $newFoo->bar with { name: > 'Robert' }}; > > How desirable that is, and how it fits with the use cases in Larry's > post, I'm not sure. > > > Regards, > > -- > Rowan Tommins > [IMSoP] > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php > >
Good breakdown. One benefit of records is that they can be structurally typed (instead of nominally, as classes are), but that's probably never going to happen in PHP. :) Perhaps a `readonly` attribute is best for now? Compare with the annotation supported by Psalm: https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-readonly-and-readonly Olle

Thomas Nunninger

5 years ago
Am 28.12.20 um 21:23 schrieb Larry Garfield:
> There's been a number of discussions of late around property visibility and how to make objects more immutable. Since it seems to have been well-received in the past, I decided to do a complete analysis and context of the various things that have been floated about recently. > > The full writeup is here: > > https://peakd.com/hive-168588/@crell/object-properties-and-immutability > > I hope it proves stimulating, at least of discussion and not naps.
A really nice writeup and interesting to read. But I have a question:
> We then end up with the following combinations: > > * public read, private write > * public read, private read, init write > * public none, private write > * public none, private read > * public none, private read, init write
What is the difference between (a) "public none, private read" and (b) "public none, private read, init" write"? When will (a) be initialized? And if there is really a useful case for (a) why is there no "public read, private read"? Regards Thomas

Nikita Popov

5 years ago
On Mon, Dec 28, 2020 at 9:24 PM Larry Garfield <larry@garfieldtech.com> wrote:
> There's been a number of discussions of late around property visibility > and how to make objects more immutable. Since it seems to have been > well-received in the past, I decided to do a complete analysis and context > of the various things that have been floated about recently. > > The full writeup is here: > > https://peakd.com/hive-168588/@crell/object-properties-and-immutability > > I hope it proves stimulating, at least of discussion and not naps. >
Thanks for the analysis Larry! I want to add a couple of thoughts from my side. First of all, I think it's pretty clear that "asymmetric visibility" is the approach that gives us most of what we want for the least amount of effort. Asymmetric visibility has clear semantics, is (presumably) trivial to implement, and gives immutability guarantees that are "good enough" for most practical purposes. It's the pragmatic choice, and PHP is all about pragmatism... That said, I don't think that asymmetric visibility is the correct solution to this problem space -- I don't think asymmetric visibility is ever (or only very rarely) what we actually want, it's just a good enough approximation. Unfortunately, the alternatives are more complex, and we have a limited budget on complexity. Here are the pieces that I think would make up a proper solution to this space: 1. initonly properties. This is in the sense of the previous "write once properties" proposal, though initonly is certainly the better name for the concept. Initonly properties represent complete immutability both inside and outside the class, and I do believe that this is the most common form of immutability needed (if it is needed at all). Of course, as you correctly point out, initonly properties are incompatible with wither patterns that rely on clone-then-modify implementations. I think that ultimately, the "wither pattern" is an artifact of the fact that PHP only supports objects with by-handle semantics. The "wither pattern" emulates objects with by-value semantics, in a way that is verbose and inefficient. I do want to point out that your presentation of copy-on-write when it comes to withers is not entirely correct: When you clone an object, this will always result in a full copy of the object, including all its properties. If you call a sequence of 5 wither methods, then this will create five objects and perform a copy of all properties every time. There is really no copy-on-write involved here, apart from the fact that property values (though not the property storage) can still be shared. 2. This brings us to: Objects with by-value semantics. This was discussed in the thread, but I felt like it was dismissed a bit prematurely. Ultimately, by-value semantics for objects is what withers are emulating. PSR-7 isn't "immutable", it's "mutable by-value". "Immutable + withers" is just a clumsy way to emulate that. If by-value objects were supported, then there would be no need for wither methods, and the "clone-then-modify" incompatibility of initonce properties would not be a problem in practice. You just write $request->method = 'POST' and this will either efficiently modify the request in-place (if you own it) or clone it and then modify it (if it is shared). Another area where by-value objects are useful are data structures. PHP's by-value array type is probably one of those few instances where PHP got something right in a major way, that many other languages got wrong. But arrays have their own issues, in particular in how they try to service both lists and dictionaries at the same time, and fail where those intersect (dictionaries with integer keys or numeric string keys). People regularly suggest that we should be adding dedicated vector and dictionary objects, and one of the issues with that is that the resulting objects would follow the usual by-handle semantics, and would not serve as a mostly drop-in replacement for arrays. It is notable that while HHVM/Hack initially had vec and dict object types, they later created dedicated by-value types for these instead. 3. Property accessors, or specifically for your PSR-7 examples, guards. The __clone related issues you're mostly dealing with in your examples are there because you need to replicate the validation logic in multiple places. If instead you could write something like public string $method { guard($version) { if (!in_array($version, ['1.1', '1.0', '2.0'])) throw new InvalidArgumentException; } } then this would ensure consistent enforcement of the property invariants regardless of how it is set. Circling back, while I think that a combination of these features would be the "proper" solution to the problem, they also add quite a bit of complexity. Despite what I say above, I'm very much not convinced that adding support for by-value objects is a good idea, due to the confusion that two different object semantics could cause, especially if writing operations on them are not syntactically distinct. I've written up an initial draft for property accessors at https://wiki.php.net/rfc/property_accessors, but once again I get the distinct impression that this is adding a lot of language complexity, that is possibly not justified (and it will be more complex once inheritance is fully considered). Overall, I'm still completely unsure what we should be doing :) Regards, Nikita

Pierre

5 years ago
Le 03/02/2021 à 15:14, Nikita Popov a écrit :
> > I've written up an initial draft for property accessors at > https://wiki.php.net/rfc/property_accessors, but once again I get the > distinct impression that this is adding a lot of language complexity, that > is possibly not justified (and it will be more complex once inheritance is > fully considered). > > Overall, I'm still completely unsure what we should be doing :) > > Regards, > Nikita
Hello, I love pretty much everything of this draft, it will allow to write value types in a very concise manner. Various notes thought: * Visibility modifier (public, protected, private) is useless and could be dropped entirely (I don't like var, but if that's necessary to keep it OK) for properties with asymmetric visibility directives, I don't know if the current parser will let you do that easily, but that would be a huge win for developers (even more concise code). * I love the fact that it can be combined with constructor promotion. * I love the guard and lazy features as proposed. Regarding inheritance, obviously the most important point is that interface or class contracts should not be changed, so you may open for reading a closed property, but you may not close a readable property for example. This is true for writing as well of course. You're saying basically that a get'ed property would be passed by-value and thus it would forbid indirect access such as adding values to an array ? But what if the compiler could detect that get; is just get and not a function behind and compile opcodes as if it was a normal property (I don't know Zend internals at all, just guessing here) and considers that any other more complex getter to just be incompatible ? I guess that in languages such as C# that implement such asymmetric visibility mechanism, they always return object references, so this kind of problem just doesn't exist. Thank you so much for this draft, I love the path it follows. Regards,
-- Pierre

Larry Garfield

5 years ago
On Wed, Feb 3, 2021, at 8:14 AM, Nikita Popov wrote:
> On Mon, Dec 28, 2020 at 9:24 PM Larry Garfield <larry@garfieldtech.com> > wrote: > > > There's been a number of discussions of late around property visibility > > and how to make objects more immutable. Since it seems to have been > > well-received in the past, I decided to do a complete analysis and context > > of the various things that have been floated about recently. > > > > The full writeup is here: > > > > https://peakd.com/hive-168588/@crell/object-properties-and-immutability > > > > I hope it proves stimulating, at least of discussion and not naps. > > > > Thanks for the analysis Larry! I want to add a couple of thoughts from my > side. > > First of all, I think it's pretty clear that "asymmetric visibility" is the > approach that gives us most of what we want for the least amount of effort. > Asymmetric visibility has clear semantics, is (presumably) trivial to > implement, and gives immutability guarantees that are "good enough" for > most practical purposes. It's the pragmatic choice, and PHP is all about > pragmatism... > > That said, I don't think that asymmetric visibility is the correct solution > to this problem space -- I don't think asymmetric visibility is ever (or > only very rarely) what we actually want, it's just a good enough > approximation. Unfortunately, the alternatives are more complex, and we > have a limited budget on complexity. > > Here are the pieces that I think would make up a proper solution to this > space: > > 1. initonly properties. This is in the sense of the previous "write once > properties" proposal, though initonly is certainly the better name for the > concept. Initonly properties represent complete immutability both inside > and outside the class, and I do believe that this is the most common form > of immutability needed (if it is needed at all). > > Of course, as you correctly point out, initonly properties are incompatible > with wither patterns that rely on clone-then-modify implementations. I > think that ultimately, the "wither pattern" is an artifact of the fact that > PHP only supports objects with by-handle semantics. The "wither pattern" > emulates objects with by-value semantics, in a way that is verbose and > inefficient. > > I do want to point out that your presentation of copy-on-write when it > comes to withers is not entirely correct: When you clone an object, this > will always result in a full copy of the object, including all its > properties. If you call a sequence of 5 wither methods, then this will > create five objects and perform a copy of all properties every time. There > is really no copy-on-write involved here, apart from the fact that property > values (though not the property storage) can still be shared. > > 2. This brings us to: Objects with by-value semantics. This was discussed > in the thread, but I felt like it was dismissed a bit prematurely. > > Ultimately, by-value semantics for objects is what withers are emulating. > PSR-7 isn't "immutable", it's "mutable by-value". "Immutable + withers" is > just a clumsy way to emulate that. If by-value objects were supported, then > there would be no need for wither methods, and the "clone-then-modify" > incompatibility of initonce properties would not be a problem in practice. > You just write $request->method = 'POST' and this will either efficiently > modify the request in-place (if you own it) or clone it and then modify it > (if it is shared). > > Another area where by-value objects are useful are data structures. PHP's > by-value array type is probably one of those few instances where PHP got > something right in a major way, that many other languages got wrong. But > arrays have their own issues, in particular in how they try to service both > lists and dictionaries at the same time, and fail where those intersect > (dictionaries with integer keys or numeric string keys). People regularly > suggest that we should be adding dedicated vector and dictionary objects, > and one of the issues with that is that the resulting objects would follow > the usual by-handle semantics, and would not serve as a mostly drop-in > replacement for arrays. It is notable that while HHVM/Hack initially had > vec and dict object types, they later created dedicated by-value types for > these instead. > > 3. Property accessors, or specifically for your PSR-7 examples, guards. The > __clone related issues you're mostly dealing with in your examples are > there because you need to replicate the validation logic in multiple > places. If instead you could write something like > > public string $method { > guard($version) { > if (!in_array($version, ['1.1', '1.0', '2.0'])) throw new > InvalidArgumentException; > } > } > > then this would ensure consistent enforcement of the property invariants > regardless of how it is set. > > Circling back, while I think that a combination of these features would be > the "proper" solution to the problem, they also add quite a bit of > complexity. Despite what I say above, I'm very much not convinced that > adding support for by-value objects is a good idea, due to the confusion > that two different object semantics could cause, especially if writing > operations on them are not syntactically distinct. > > I've written up an initial draft for property accessors at > https://wiki.php.net/rfc/property_accessors, but once again I get the > distinct impression that this is adding a lot of language complexity, that > is possibly not justified (and it will be more complex once inheritance is > fully considered). > > Overall, I'm still completely unsure what we should be doing :) > > Regards, > Nikita
Thanks for the feedback, Nikita. And yes, on the larger scale I'm not sure what the perfect solution is either. :-) Regarding your comments first: I've thought about "record" types in the past (by-value formal structures), which would go back to by-value semantics. However, every time I think about what features we'd want them to have, I always end up back at "every possible feature of classes someone will want on records," at which point we're just double-implementing classes on a new zval type. That seems ungood. (Imagine figuring out how to do generics, and then needing to do them twice.) The alternative would be some kind of "by-value-passing" flag on class definitions, something like "byval class Foo { ... }", but I have absolutely no idea if that's even possible (at the engine level) much less desireable (at the API predictability level). To some extent you want to be able to predict in advance whether a variable will pass by value or by reference or by handle so you know what it's safe to do to it. It's also trivial to bypass by-value by passing a value by reference, thus losing all the safety that would give you. See also: Any Drupal version in the last 15 years, that *loves* passing around enormous arrays by reference so they can be modified. The question, though, is if we want immutable values or passing-safe values, which are not *quite* the same thing. You assert above that what we really want are passing-safe values. I'm... not actually sure myself which one is the true desire since they've been coupled for so long, other than modify-in-place structures don't always have good ergonomics. (I personally prefer chaining set or with methods over repeating an object name over and over again to set a value. I'm sure others will disagree.) I will note that even if we were to have a record type of some kind, initonly values still pose a challenge if they're derived from some other value that may change, or in cases where an object still can and should be cloned for reasons other than emulating immutability. Also, there are other reasons to implement vec and dict in the engine beyond just enforcing immutability, although I would want to do that even if they were done with a record type. Regarding your property accessor proposal: I've always said that initonly, asymmetric visibility, etc. are all stepping stones toward full property accessors. My understanding was that they failed before for performance reasons. If you believe those are solvable in a way that would let us skip the intermediary steps and go straight toward the full package (which would effectively let us emulate all of these other features we've been discussing), I am so totally here for it. I adore the idea of a guard method on properties. That would be useful in a huge number of places, even if nothing else makes it in. I have only 2 concerns about them: 1) I can see them being used a ton on promoted properties, so guard clauses being incompatible with promoted properties would be extremely sad. We should spend some time exploring ways to make them play nice together. 2) There are likely a huge number of cases that can be reduced to a declarative syntax, which could then be parsed, extracted, and used for creating tests, creating JS equivalents for automated form validation, and so on. A method wouldn't support that, but offers more flexibility. Which... Those two together just gave me an idea. Make it an attribute. class Foo { public function __construct( #[GuardMethod('startsWithNumber')] #[GuardRegex('[0-9]')] public string $bar ) { } public function startsWithNumber($val): bool { ... } } That would allow some validation to be baked in, in a declarative form, support arbitrary method guards, and move the code away so that it's compatible with constructor promotion. It could potentially be implemented in a way that is user-space extensible, too. I think this is worth investigating further, even independently of everything else we're discussing. I also love lazy/init/whatever properties, as that gives us the self-memoization that no other option discussed has managed. As noted above, my only concern is what happens to it if some other value it is computed off of changes, or the object is cloned, etc. One viable answer is "if it's not safe to memoize then just don't do that, dummy," which may be the answer, but as we all know PHP developers in the wild do not always think such things through. (And it's such a tempting feature that it may get over used, and get people into trouble.) Again, possibly not something we could realistically resolve but worth calling out. The descriptions around the backing property are a bit clunky. I think I follow/agree with what you're describing, but the way it's described with the underscore property is a bit misleading. I also think that making $value a magic name is not a good approach. That's not at all self-evident from context; you just have to know that is a magic value now. I would alternatively propose using the property name itself. So: class Test { public string $prop { get { return $this->prop; } set { $this->prop = $prop; } } } That way the name is predictable and logical. I don't have any good ideas on the inheritance or references front at the moment. --Larry Garfield