Immutability in JavaScript

Christian Johansen
Share

Immutability is a core principle in functional programming, and has lots to offer to object-oriented programs as well. In this article, I will show what exactly immutability is all about, how to use this concept in JavaScript, and why it’s useful.

What is Immutability?

The text-book definition of mutability is liable or subject to change or alteration. In programming, we use the word to mean objects whose state is allowed to change over time. An immutable value is the exact opposite – after it has been created, it can never change.

If this seems strange, allow me to remind you that many of the values we use all the time are in fact immutable.

var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);

I think no one will be surprised to learn that the second line in no way changes the string in statement. In fact, no string methods change the string they operate on, they all return new strings. The reason is that strings are immutable – they cannot change, we can only ever make new strings.

Strings are not the only immutable values built into JavaScript. Numbers are immutable too. Can you even imagine an environment where evaluating the expression 2 + 3 changes the meaning of the number 2? It sounds absurd, yet we do this with our objects and arrays all the time.

In JavaScript, Mutability Abounds

In JavaScript, strings and numbers are immutable by design. However, consider the following example using arrays:

var arr = [];
var v2 = arr.push(2);

What is the value of v2? If arrays behaved consistently with strings and numbers, v2 would contain a new array with one element – the number 2 – in it. However, this is not the case. Instead, the arr reference has been updated to contain the number, and v2 contains the new length of arr.

Imagine an ImmutableArray type. Inspired by strings and numbers behavior, it would have the following behavior:

var arr = new ImmutableArray([1, 2, 3, 4]);
var v2 = arr.push(5);

arr.toArray(); // [1, 2, 3, 4]
v2.toArray();  // [1, 2, 3, 4, 5]

Similarly, an immutable map, which can be used in place of most objects, would have methods to “set” properties that don’t actually set anything, but return a new object with the desired changes:

var person = new ImmutableMap({name: "Chris", age: 32});
var olderPerson = person.set("age", 33);

person.toObject(); // {name: "Chris", age: 32}
olderPerson.toObject(); // {name: "Chris", age: 33}

Just like 2 + 3 does not change the meaning of either numbers 2 or 3, a person celebrating their 33rd birthday does not change the fact that they used to be 32.

Immutability in JavaScript in Practice

JavaScript doesn’t (yet) have immutable lists and maps, so we’ll need a third-party library for now. There are two very good ones available. The first one is Mori, which enables the use of ClojureScript’s persistent data structures and supporting APIs in JavaScript. The other one is immutable.js, written by developers at Facebook. For this demonstration I will use immutable.js, simply because its API is more familiar to JavaScript developers.

For this demonstration, we’ll look at how to work with Minesweeper with immutable data. The board is represented by an immutable map where the most interesting piece of data is the tiles. It’s an immutable list of immutable maps, where each map represents a tile on the board. The whole thing is initialized with JavaScript objects and arrays, and then immortalized by immutable.js fromJS function:

function createGame(options) {
  return Immutable.fromJS({
    cols: options.cols,
    rows: options.rows,
    tiles: initTiles(options.rows, options.cols, options.mines)
  });
}

The rest of the core game logic is implemented as functions that take this immutable structure as their first argument, and returns a new instance. The most important function is revealTile. When called, it’ll flag the tile to reveal as revealed. With a mutable data structure, that would be very easy:

function revealTile(game, tile) {
  game.tiles[tile].isRevealed = true;
}

However, with the kind of immutable structures proposed above, it would turn into a bit more of an ordeal:

function revealTile(game, tile) {
  var updatedTile = game.get('tiles').get(tile).set('isRevealed', true);
  var updatedTiles = game.get('tiles').set(tile, updatedTile);
  return game.set('tiles', updatedTiles);
}

Phew! Luckily, this sort of thing is quite common. So, our toolkit provides methods for that:

function revealTile(game, tile) {
  return game.setIn(['tiles', tile, 'isRevealed'], true);
}

Now the revealTile function returns a new immutable instance, where one of the tiles is different from the previous version. setIn is null-safe and will pad with empty objects if any part of the key does not exist. This is not desirable in the case of the Minesweeper board, because a missing tile means we’re trying to reveal a tile outside the board. This could be mitigated by using getIn to look for the tile before manipulating it:

function revealTile(game, tile) {
  return game.getIn(['tiles', tile]) ?
    game.setIn(['tiles', tile, 'isRevealed'], true) :
    game;
}

If the tile does not exist, we simply return the existing game. This was a quick taste of immutability in practice, to dive deeper in, check out this codepen, which includes a full implementation of the Minesweeper game rules.

What About Performance?

You might think that this would yield terrible performance, and in some ways you’d be right. Whenever you add something to an immutable object, we need to create a new instance by copying the existing values and add the new value to it. This will certainly be both more memory intensive and more computationally challenging than mutating a single object.

Because immutable objects never change, they can be implemented using a strategy called “structural sharing”, which yields much less memory overhead than you might expect. There will still be an overhead compared to built-in arrays and objects, but it’ll be constant, and can typically be dwarfed by other benefits enabled by immutability. In practice, the use of immutable data will in many cases increase the overall performance of your app, even if certain operations in isolation become more expensive.

Improved Change-Tracking

One of the hardest tasks in any UI framework is mutation tracking. This is such a widespread challenge that EcmaScript 7 provides a separate API to help track object mutations with better performance: Object.observe(). While many people are excited about this API, others feel like it’s the answer to the wrong question. In any case, it does not properly solve the mutation tracking problem:

var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}];
Object.observe(tiles, function () { /* ... */ });

tiles[0].id = 2;

The mutation of the tiles[0] object does not trigger our mutation observer, thus the proposed mechanism for mutation tracking fails even the most trivial use-cases. How does immutability help in this situation? Given application state a, and potentially new application state b:

if (a === b) {
  // Data didn't change, abort
}

If the application state hasn’t updated, it’ll be the same instance as before, and we don’t need do anything at all. This does require that we track the reference that holds the state, but the whole problem has now been reduced to managing a single reference.

Conclusion

I hope this article has given you some background on how immutability can help you improve your code, and that the provided example can shed some light on the practical aspects of working this way. Immutability is on the rise and this will not be the last article you read on the subject this year. Give it a shot, and I promise you’ll be as excited about it as I am in no time.