The example I’m about to give is for the PHP language, but I think this scenario applies to most languages.
Let’s say I have an object called Response
and I want it to be immutable. Every method should return a new instance rather than modifying the state of the current instance. Response
s have headers, so I create a method called withHeader($name, $value)
that makes a copy of my response object, sets the header, and returns the new instance. It’s pretty simple to use:
$newResponse = $oldResponse->withHeader('Content-Type', 'application/json');
Then I decide the headers actually have quite a few properties and I’d rather separate this out into a separate object called a HeaderBag
. Now I want my Response
to hold a HeaderBag
but I still want it to be immutable.
I can’t make the the bag a public property, like $response->headers
because even if the $headers
object is immutable, the property can still be reassigned. So maybe I add something like $response->getHeaders()
which returns my HeaderBag
object, but now how do I manipulate it?
If I move my withHeader
method into this sub-object, then I wind up with something like $response->getHeaders()->withHeader(...)
. The withHeader
method would presumably return a new HeaderBag
since it too is immutable, but then what? I guess I’d end up with something like this:
$newResponse = $oldResponse->withHeaders($oldResponse->getHeaders()->withHeader($name, $value));
Which seems a bit ridiculous; to add a single value we have to clone two objects and write this abomination of a line. Sure, we can write a helper method on the Response
object to shorten it, but the whole idea was to move all the header-related methods into the HeaderBag
class.
Questions:
- Is there a better way to do this?
- If there isn’t, is it even worth the effort/cost to make objects immutable?
- If we could design a new language with immutability in mind, what would this look like?
5
An http response can naturally be thought of as immutable, but only from the moment that it has been fully constructed and issued. Before that moment, various implementations of “response” classes in various environments are generally used as builders (see Builder Pattern) which get passed around in order for various routines to add to them piecemeal all the information that constitutes the final response. So, during the lifetime of the response as a builder, it is by its very nature mutable.
Now, it is still possible to take a class which is mutable by nature and turn it into an immutable class, but what I would like to suggest to you is that although doing so might be a fabulous exercise on paper, (the entire series of Erik Lippert’s articles on immutability was nothing short of fascinating,) it is not really something that you would want to be doing in practice with any non-trivial class, because you end up with abominations precisely like the one you discovered. Plus, it is clearly not worth the effort, and it is stupendously wasteful in terms of computing resources.
So, my suggestion would be that if you really like the idea of an immutable response, (I like it too,) then rename your current response to ResponseBuilder
, let it be mutable, and when it is complete, create an immutable response out of it and discard the builder.
I will attempt to address the three questions you asked at the very bottom.
1) Is there a better way to do this?
The better way to do this is to build a generic library or language which is capable of efficiently managing the creation and updating of immutable objects. I will try to address this in the answer to the 3rd question.
2) If there isn’t, is it even worth the effort/cost to make objects immutable?
This is a hard question, but I would say that unless PHP has a robust library for this (which it may or may not, I am not familiar with PHP) purpose the answer is probably no. First, it would be inefficient and error prone if you just clone your objects. Secondly, if you are working in a language like PHP where the entire language and community is built around and used to mutability, it can be confusing for other programmers when a library is immutable.
3) If we could design a new language with immutability in mind, what would this look like?
Fortunately you wouldn’t have to design a new language to know what it would look like because it’s already been done. You could look at existing ones like Haskell or Clojure. These languages were designed with immutability in mind. Almost everything in those languages is immutable: Lists, Vectors, Maps, Sets, Records etc and they are implemented in a way which makes updates much faster than copying the entire object.
Clojure for instances uses Hash array mapped trie http://en.wikipedia.org/wiki/Hash_array_mapped_trie to implement its Vectors, Maps, Sets and Records which gives O(log(n)) insert/update speed and the underlying implementation itself is never visible to the end user (in fact small Vectors or Maps may use a completely different representation).
When almost everything in your language is immutable some of the problems you were facing go away. For instance, if the Response holds a Bag there is not such a big problem exposing the Bag because the Bag is immutable too. Also, when you create a new Response where the status code changed, but the headers were the same, you don’t need to copy the headers Bag, you can just create a pointer to it because that Bag and everything contained in it is immutable! Allowing the compiler, runtime and libraries to make assumptions about the immutability of other things in the language is part of what allows languages designed with immutable data structures to be more efficient and more usable than trying add immutability as an afterthought to a language which started off mutable.
This is actually how the popular Ring framework https://github.com/ring-clojure/ring works in Clojure. A response is just a Map:
{:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"}
As you can see, the response is a Map, and the headers are just another Map. What it would look like to produce a new Map with Content-Type=”text/xml” would be:
(assoc-in response [:headers "Content-Type"] "text/xml")
The assoc-in
function is from the standard library which does the work from the “ridiculous” example of knowing how to get at the nested Map and rebuild the data structures it was a part of so that the programmer doesn’t need to make the explicit calls to your withHeader
and withHeaders
functions. There are also numerous other techniques and functions which make common operations easier. For instance if we had more than one update we wanted to make to the response we might use the ->
or thread-first macro.
(-> response
(assoc-in [:headers "Content-Type"] "text/xml")
(assoc-in [:headers "Content-Disposition"] "attachment; filename="data.xml""))
There are also many methods in the standard libraries for these languages which could apply a function to a value in a nested structure, merge two immutable Maps together, compare Maps for equality, etc.
2
Disclaimer: I wrote this as I was coming up with it
I think ideally we would have read-only properties, like in C#. Then we could expose $response->headers
without worrying it’ll be re-assigned. Then what we need is a way for HeaderBag::withHeader
to return a (new) Response
object, but only when we’ve accessed it via the response object — not if we’re working with it on its own. i.e. this would return a new Response
object,
$newResponse = $oldResponse->headers->withHeader('Content-Type', 'text/xml')
but this would return a HeaderBag
object:
$newHeaders = $standaloneHeaders->withHeader('Content-Type', 'text/xml');
This of course would be super confusing if the method returned different objects depending on how it was called, so we need fix up the syntax a bit to make it more clear…
What if we had a new “with” operator (ω) that behaves a bit like assignment (=) except it returns a new instance of the object on the left with the new property set. For example, instead of
obj.x = 5
You would write
obj2 = obj ω x = 5
Better yet, you could manipulate several properties at once while only making one copy of the object:
obj2 = obj ω x = 5, y = 6
Then going back to our example above we could write:
$newResponse = $oldResponse ω headers = $oldResponse->headers->withHeader('Content-Type', 'text/xml');
But that’s not much better. Maybe we can add another symbol to represent the object being manipulated? For example, if you wanted to increment a value, you could either write:
obj2 = obj ω x = obj.x + 1
Or
obj2 = obj ω x = τ + 1
Where τ
refers to the property just after the ω
. Now we can write:
$newResponse = $oldResponse ω headers = τ->withHeader('Content-Type', 'text/xml');
Getting better, but I think we can still improve it. Let’s pretend our HeaderBag
implements an array interface, such that we can get a header via $response->headers['Content-Type']
.
$newResponse = $oldResponse ω headers = τ ω τ['Content-Type'] = 'text/xml';
Now let’s see what it looks like to set a whole bunch of properties all at once:
$newResponse = $oldResponse
ω headers = τ ω τ['Content-Type'] = 'text/xml',
τ['Content-Disposition'] = 'attachment; filename="data.xml"',
status = 200,
body = '<root>text</root>'
I think I might need some parentheses in there, or maybe I should remove the ,
operator and require more ω
s, I’m not sure, but that’s the general idea.
We might want to replace ω
with something a bit easier to type, perhaps w/
? For τ
I was thinking “this” but that has a different meaning inside of classes.
This needs a bit more thought, but I welcome your feedback. WuHoUnited gave some examples of manipulating nested objects in Clojure, but I’m not sure if it’s as flexible as my solution which lets you call any method at any point in the chain.
With this semi-ideal solution in mind, we can try converting it back into syntactically valid PHP:
$newResponse = $oldResponse->withProp(['headers' => function($headers) {
return $headers->withKey(['Content-Type' => 'application/json']);
]);
Something like that. Implementation would look like:
<?php
trait WithTrait {
public function withProp($data) {
$new = clone $this;
foreach($data as $prop=>$cb) {
$new->{$prop} = $cb($this->{$prop});
}
return $new;
}
public function withKey($data) {
$new = clone $this;
foreach($data as $key=>$cb) {
$new[$key] = $cb($this[$key]);
}
return $new;
}
}
I think this could work, but I don’t think I want to go down this road when Mike’s solution is so much simpler.