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

Reply via email to