On Mon, Dec 28, 2020 at 9:24 PM Larry Garfield <la...@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