Javascript Snippets: Immutable Lists

2015-04-27

With the recent popularity of view frameworks such as React and Mercury, immutable data structures have become widespread choices for storing UI state. In this short post, we’ll examine the motivation for maintaining immutability and build a simple snippet for constructing reasonably performant immutable lists. For production usage, check out Facebook’s immutable.js.

Why Immutability?

In various view frameworks, view functions are just plain Javascript functions which take a object representing state and return a virtual HTML tree:

function listView(list) {
  return h('ul', list.map(item => h('li', item)));
}

If listView is used as a component of a large web-app, the list argument may be a nested property of a more complex state object. When the contents of list change, we want to re-render the list to reflect the new state, but when other properties of that object change, it would be nice to not have to re-compute the list view.

In the second case, how can we see if the list argument has changed in a performant fashion? If list is a plain Javascript array and gets mutated elsewhere by list[i] = val, then the equality value of list itself has not changed. Thus, we can’t do a simple object equality === check but instead need the previous contents of the list, and equality checking would be $O(n)$ in the length of the list.

Instead, we may be willing to take a slight performance hit on gets, sets, and iteration as long as equality checking takes constant time. We can use immutable data structures: structures type of T such that T.set returns a new object of type T (if the set changes the contents).

A Naive Implementation

Below is a naive implementation where the list is copied

class List {
  constructor(array) {
    this.array = array;
  }

  get(key) {
    return this.array[key];
  }

  set(key, val) {
    var newArray = this.array.slice();
    newArray[key] = val;
    return new List(newArray);
  }

  iter*() {
    for (var i = 0; i < this.array.length; i++) {
      yield this.array[i];
    }
  }
}

Note here that while get is $O(1)$, set is $O(n)$ in the length of the list. Can we do better?

Trees

How can we improve the performance of set? Note that above, we are forced to copy the entire list into a new list. If we change our underlying data structure from a flat list to a tree, it turns out that we can get $O(\log n)$ sets.

There are two caveats: the first is that using trees slows gets to $O(\log n)$, and the second is that we incur a possibly nontrivial constant factor (around equal to branch size) for sets since we must copy the unchanged children of every tree node we touch.

var BRANCH = 8;
var BRANCH_EXP = 3; // 2^3 = 8 - used for bit shifts.
class Tree {
  constructor() {
    this.children = [];
    this.depth = 0;
  }

  get(key) {
    if (this.depth === 0 && key < BRANCH) {
      return this.children[key];
    } else {
      var shift = BRANCH_EXP * this.depth;
      var childSize = 1 << shift;
      return this.children[key >>> shift].get(key % childSize);
    }
  }

  set(key, val) {
    var newChildren = this.children.slice();
    if (this.depth === 0 && key < BRANCH) {
      newChildren[key] = val;
    } else {
      var shift = BRANCH_EXP * this.depth;
      var childSize = 1 << shift;
      var childIndex = key >>> shift;
      newChildren[childIndex] = newChildren[childIndex].set(key % childSize, val);
    }
    return newChildren;
  }
}

We can now define the List class as follows:

class List {
  constructor(array) {
    if (array isinstance Tree) {
      this.tree = array;
    } else {
      this.tree = makeTree(array);
    }
  }

  get(key) {
    return this.tree.get(key);
  }

  set(key, val) {
    return new List(this.tree.set(key, val));
  }

  iter*() {
    // Inorder transversal of tree.
  }
}

Next Steps

There are lots of performance improvements which can be made over the previous section’s code, which was written with a goal of concision. In a future post, we’ll examine some of these and do some performance testing.