If you're like me, you've heard a lot of people say things like "you shouldn't mutate state!" and talk a lot about things being mutable and how that's bad. This is a short post to explore why.
First, meaning. Mutable = can change. Immutable = can't be changed. The most common use of "immutable" in programming is to refer to an object whose properties cannot be changed after creation.
Why do people often see mutability as bad?
Let's look at a fairly realistic example showing one of the failings of mutability:
The task is to take a date that the user entered and return either "yesterday", "today" , "tomorrow", or just return the date.
Here's a simple implementation in a fictional PHP-like language:
$usersDate = new Date($usersInput);
$today = new Date();
$yesterday = $today->subtractDays(1);
$tomorrow = $today->addDays(1);
if ($usersDate->isSameDate($yesterday))
return "yesterday";
if ($usersDate->isSameDate($today))
return "today";
if ($usersDate->isSameDate($tomorrow))
return "tomorrow";
return $usersDate->toString();
This should work as you expect, right? Not necessarily. The implementation of the Date
class matters. If the Date
class was implemented in an immutable way, this should be fine. Otherwise, you'd get either "yesterday" or the user's date. Why? Here's what a mutable implementation could look like (with veryyy simpliifed logic):
class Date
{
public function subtractDays($days) {
$this->day = $this->day - $days;
return $this;
}
}
And an immutable implementation:
class Date
{
public function subtractDays($days) {
return new Date($this->getYear(), $this->getMonth(), $this->day - $days);
}
}
(The addDays()
method would be implemented in a similar manner.)
The key difference here: the mutable version changes the properties of the Date instance and returns the same instance, while the immutable version returns a new instance with the correct properties. Here's what our example earlier actually executes with a mutable Date:
$today = new Date();
$yesterday = $today->subtractDays(1);
// ^-- $yesterday and $today are the same date—yesterday!
$tomorrow = $today->addDays(1);
// ^-- Now, $yesterday, $today and $tomorrow are the same date—today! 😳
// All 3 test dates are the same, so if this fails/passes, same with the rest
if ($usersDate->isSameDate($yesterday))
return "yesterday";
if ($usersDate->isSameDate($today))
return "today";
if ($usersDate->isSameDate($tomorrow))
return "tomorrow";
return $usersDate->toString();
Ouch! This is a real life problem that has frustrated many developers. It's why PHP added a DateTimeImmutable
class. Here's a real PHP version of this example.
So, how would you work around this situation? You could switch to an immutable implementation, like using DateTimeImmutable
in PHP instead of DateTime
. If that isn't available, you have to remember to make copies of the objects before modifying. Something like this:
$today = new Date();
$yesterday = (new Date($today))->subtractDays(1);
$tomorrow = (new Date($today))->addDays(1);
Another way mutability can bite you in the ass would be if you pass a mutable object to a function and that function modifies it without your knowledge.
$order = Order::create($data);
// Unknown to you, this function modifies the order you passed to it
checkIfCouponCodeIsValidForOrder($order, $couponCode);
// continue working with $order
Again, this means you need to manually clone the object before passing, or make sure you're passing an object that restricts modifications to it.
Many programming languages pass objects by reference (because it's less expensive), so a function that receives an object parameter will get the same object you have, not a copy. This means that the function can modify it freely (if the object allows).
How do you ensure immutability?
First off, you need to implement practices that favour immutability. You should design your objects to favour modifying their properties only at creation time. Actions like re-assigning variables and modifying object properties are frowned on in the world of immutability. There are even ESLint rules that forbid such reassignments to prevent you shooting yourself in the foot. Note that there's a performance penalty to always cloning objects just to avoid modifying them directly. This penalty is usually negligible, though, until you're dealing with hundreds of operations or very large objects.
On the flip side, if you're writing mutable code, such as a function that modifies its arguments, you should clearly indicate that this will happen. For instance, by naming a method setDay()
, it becomes obvious that the method is mutable and will change the day on the same instance.
If you want to go deeper, there are libraries that help with this. Some advantages these libraries provide:
- better performance than hand-rolling your own
- cleaner code than always copying or cloning an object before modification
There are two popular ones for JavaScript: Immutable.js and immer. Immer is a bit more involved than Immutable.js because you have to modify how you write your code, making you use producers and draft states. Immutable.js gives you new data structures to use instead of JavaScript's mutable ones, but you interact with them in the same way.
(Note: Immutable.js is apparently unmaintained as at October 2020.)
For PHP, there are a few libraries, but immutability hasn't really caught on as much. I believe mutability is a much more serious issue on the frontend, especially with the proliferation of JavaScript-heavy apps passing state around. Since PHP doesn't even hold state beyond a single request, the effect of mutability is much less.
Personally, I haven't used any immutability libraries because the tradeoff hasn't been worth it for me. I'm not an immutability purist (shout out to the functional programming guys🙂), and mutability hasn't been so much of a problem for me, especially as I hardly work with front-end frameworks. I often just avoid it by taking note of where and how I'm passing my variables around.
If you're looking for more reading on immutability, I suggest checking out the docs for Immer and Immutable.js. There are also lots of excellent posts in the wild, like this and this.
Top comments (7)
Awesome.
You should really write more.
Hopefully I will. 😄
While I agree with your programming principles in the OO context of things. Its not really what the immutability game plays out in the functional or concurrent/parallel worlds.
The big deal about immutability is when you deal with parallelism. If you don't allow mutation in your code then you can use many threads to do the same work. In functional code its also about eliminating side effects that don't allow you to make optimizations such as f(x) + f(x) = 2 f(x).
What you are discussing is just good OO programming practice so no inadvertent things happen to your encapsulated objects. In commercial programming you should always make it hard for other programmers (including yourself) to break your program. I've been known to add traps in the code so if you do changes to the code it will explode. Like
counter = 0;
while (new = transform(old) ) {
// lots of logic
// trap
if (++counter > 10_000) throw new AssertionFailedError("too many iterations");
}
(you can also change the while into a for statement but that's besides the point)
Hehe. I'm aware that immutability helps with thread-safety. Just didn't want to go there in this post (plus I haven't really had many such issues). This is not a post about functional programming or about the whole immutability philosophy. Just wanted to give folks who've heard the term but don't know what it is an understanding. That's why I linked to those other posts at the end.
This is a good explanation, thank you!
Glad you found it helpful.😄
Nice. I respect functional programming principles—but from a distance.😅 I'm not sold on everything, but I like seeing some of their reasoning, especially about types and values.