What's the big deal about immutability?

Thursday, April 16, 2020

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.

Powered By Swish