On 11/16/15 5:11 PM, Mathieu Rochette wrote:
On 11/16/2015 11:11 PM, Larry Garfield wrote:
On 11/16/15 3:15 AM, Chris Riley wrote:
Immutable on a property makes the property immutable once it takes on a
none null value. Attempts to modify the property after this results
in a
fatal error.
Any thoughts?
~C
As a firm believer in the value of immutable data structures, I don't
believe such a simple approach would be useful and would likely be
counter-productive.
agreed
The trick is there are 2 cases for immutability: An immutable service
object, and an immutable data object.
A service object should be immutable once it's setup. However, that
setup *may*, in some cases, involve setter injection. That's inferior
or constructor injection but that is not always feasible. In
practice, I'm not sure we need new syntax here at all.
I'm curious here, why is construction not always enough ? do you have
a use case in mind ?
There are various cases, like a logger, a container-aware service (such
as factories), or other cases where you may want a class to opt-in to a
specific dependency by interface. In that case your object creation
process (DI Container or otherwise) can do something like this:
$object = create_object_via_constructor_injection(...);
if ($object instanceof Loggable) {
$object->setLogger($this->getSystemLogger());
}
There are also cases in, eg, Drupal where forcing constructor injection
for a class that is extending another results in unnecessary and
annoying boilerplate. (I'm thinking of our new Plugin system.) As I
said, it's suboptimal but use cases for setter injection (really
interface injection) do exist, so a language-strict enforcement of
constructor-or-nothing are not always applicable.
In any event, I don't think service objects really need this feature
anyway as there's no data on the service to access; if it had any, it
wouldn't be a service object. :-)
For data objects, this is where it gets interesting. Data objects
*do* need to be modifiable for a given context... for that context.
Immutable data objects are, largely, useless and in my experience
harmful unless they have a ->giveNewVersionWithThisOneChange()
method. In PSR-7, that's the with*() methods. DateTimeImmutable has
the same methods as DateTime, but they return new instances rather
than modifying the invoked object (despite their confusing names,
which are like that for historical reasons).
How does that makes them mutable in a "given context" ? the with*()
can always create a new object with everything in the constructor, am
I missing something ?
The "everything in the constructor" is the problem. That results in,
essentially, an obscenely long function call that just happens to be
named __construct(). If I wanted something that obscure and hard to
work with I'd just use anonymous arrays. :-)
Consider PSR-7's ServerRequest object. It's immutable. However, when
passing it from one middleware to the next you want to be able to pass a
"modified" version of it, containing extra properties that indicate the
result of routing, for example. Or you have a middleware that does
cookie encryption, so it needs to "modify" the cookie header.
If that meant each middleware needed to do something like this:
function routing_middleware(ServerRequest $r, callable $next) {
$attributes = $r->getAttributes();
$attributes['controller'] = 'foo';
$new = new ServerRequest($r->getMethod(), $r->getUri(),
$r->getHeaders(), $r->getBody(), $r->getAttributes());
return $next($new);
}
Well, you can see how gross that is, and it gets worse as soon as you
need to modify more than one property. You essentially need to extract()
the object back to primitives and build a new one, which is more
cumbersome, more verbose, slower, and makes kittens cry. At this point
it's easier to just have a big anonymous array and pass that around;
it's no less self-documenting and easier to work with.
(I had to work with such an object once when doing a Symfony REST
project, because that's what the HATEOAS bundle did. It was impossible
to work with.)
Instead, with the with*() methods, PSR-7 lets you do this:
function routing_middleware(ServerRequest $r, callable $next) {
$new = $r->withAttribute('controller', 'foo');
return $next($new);
}
Or even inline that to a single line (if you really do have a hard coded
controller, which is unlikely but other cases like cookie encryption might).
function cookie_middleware(ServerRequest $r, callable $next) {
return $next($r->withHeader('Cookie',
decode_cookie($r->getHeader('Cookie')));
}
Which looks nice and functional, too.
It's much easier to read, much easier to write, still communicates
intent, and thanks to copy-on-write is no more expensive under usual usage.
Most of the objection to immutable PSR-7 objects, including my own, was
based on the assumption that we'd be stuck with the former example,
which is a major PITA. The development of the with*() methods is what
made immutable PSR-7 feasible.
In less generic form, imagine if DateTimeImmutable worked like this:
$d = new DateTimeImmutable();
$d2 = new DateTimeImmutable($d->format('c') . ' + 1 day');
Instead of like this:
$d2 = $d->modify('+1 day');
I think you'll agree the second is much nicer.
In all of these cases, though, we get the benefit of locality of
effect. Changes to an object in one function cannot impact another
function, because it's not a change but a new object.
My ideal would be to revive the property RFC and leverage
per-property access control separately for read and write to create
externally-immutable objects. That would allow for with*() methods
to be implemented as appropriate for a given object.
are you talking about this one ?
https://wiki.php.net/rfc/readonly_properties
No, I meant this earlier one:
https://wiki.php.net/rfc/propertygetsetsyntax-v1.2
I dislike the readonly keyword for basically all the same reasons I gave
earlier in this thread. :-)
--
--Larry Garfield
--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php