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

Reply via email to