Immutable JavaScript example

Implementing Todo list with Undo using immutable data structure.

I prefer using Array.prototype.map over Array.prototype.forEach because .map semantically signals to everyone that it constructs a new array, while .forEach might modify the existing one. In order to actually prevent modifying the array in .map, one needs to use an immutable array, see Avoiding Side Effects blog post. In this blog post I will show another benefit of using an immutable list in my JavaScript programs - easy implementation of Undo command.

This is an exploration in progress of immutable data in JavaScript. I am not sure that this example clearly shows the advantages of immutability. Consider this a practice exercise. I implement a Todo list using the traditional object oriented encapsulation and the same features using an immutable data structure. I also answered a few questions at the end of this blog post; these questions poped up as I was working with immutable objects.

Todo list using OO

I will start with implementing a simple Todo list using a regular array. Each item has a label and done property.

Using individual todo items

My initial implementation allows to directly modify the state of a todo item in the collection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Todos() {
var _todos = [];
var todosModule = {
add: function (label) {
var newTodo = {
label: label,
done: false
};
_todos.push(newTodo);
return newTodo;
},
toString: function () {
return _todos.map(function (todo) {
return todo.label + ' ' + (todo.done ? 'done' : '');
}).join('\n');
}
};
return todosModule;
}

The above code makes marking an individual item as done very convenient

// typical use
var todos = Todos();
var doFoo = todos.add('foo');
var doBar = todos.add('bar');
console.log(todos.toString());
// foo
// bar
doFoo.done = true;
console.log(todos.toString());
// foo done
// bar
doBar.done = true;
console.log(todos.toString());
// foo done
// bar done

Unfortunately I do not see how to implement todos.undo() if the list has no idea what has happened. I will need to move done into the todos object

Using Todos API to mark item done

function Todos() {
  var _todos = [];
  var todosModule = {
    add: function (label) {
      _todos.push({
        label: label,
        done: false
      });
      return todosModule;
    },
    done: function (index) {
      _todos[index].done = true;
      return todosModule;
    },
    toString: function () {
      return _todos.map(function (todo) {
        return todo.label + ' ' + (todo.done ? 'done' : '');
      }).join('\n');
    }
  };
  return todosModule;
}

The Todos object only has 3 methods add, done and toString. One can easily add tasks, print them, and mark an individual task as done.

var todos = Todos();
todos
  .add('foo')
  .add('bar');
console.log(todos.toString());
// foo
// bar
todos.done(0);
console.log(todos.toString());
// foo done
// bar
todos.done(1);
console.log(todos.toString());
// foo done
// bar done

There is only a single reference todos always pointing at the same object. When we call todos.add we modify the only instance. This makes implementing undo simple.

clearDone

If we mark items done, let us clear the list of done items too.

clearDone: function () {
  _todos = _todos.filter(function (todo) {
    return !todo.done;
  });
  return todosModule;
}
// use clearDone
var todos = Todos();
todos.add('foo').add('bar');
todos.done(0);
console.log(todos.toString());
// foo done
// bar
todos.clearDone();
console.log(todos.toString());
// bar

Undo clearDone command

We will keep track of done calls using list of commands. Each command is an object that can reverse a previous todos list modification. An example of a command with undo:

var sum = 0;
function add(a) {
    sum += a;
    return {
        undo: function () {
            sum -= a;
        }
    };
}
var undoAdd10 = add(10); // sum = 10
var undoAdd20 = add(20); // sum = 30
undoAdd20.undo(); // sum = 10
undoAdd10.undo(); // sum = 0

We should respect the action order, thus we will use a stack to keep list of commands to undo them in reverse order.

function Todos() {
  var _todos = [];
  var _commands = [];
  var todosModule = {
    add: function (label) {
      _todos.push({
        label: label,
        done: false
      });
      return todosModule;
    },
    done: function (index) {
      _todos[index].done = true;
      return todosModule;
    },
    toString: function () {
      return _todos.map(function (todo) {
        return todo.label + ' ' + (todo.done ? 'done' : '');
      }).join('\n');
    },
    clearDone: function () {
      _commands.push({
        // save entire list using undo function closure
        undo: function (prevTodos) {
          _todos = prevTodos;
        }.bind(null, _todos)
      });
      _todos = _todos.filter(function (todo) {
        return !todo.done;
      });
      return todosModule;
    },
    undo: function () {
      if (_commands.length) {
        _commands.pop().undo();
      }
    }
  };
  return todosModule;
}

we can set done, clear done tasks, then undo the clear operation

var todos = Todos();
todos.add('foo').add('bar');
todos.done(0);
// foo done
// bar
todos.clearDone();
// bar
todos.undo();
// foo done
// bar

Notice the added complexity that now is inside the Todos object. Because the undo commands need full access to the list, the undo functionality is added inside the Todos closure.

Undo any action

Our undo clearDone feature just restores the previous list. Why don't we reuse it for other actions? Let us factor it out into checkpoint function

function Todos() {
  var _todos = [];
  var _commands = [];
  function checkPoint() {
    _commands.push({
      undo: function (prevTodos) {
        _todos = prevTodos;
      }.bind(null, _todos)
    });
  }
  var todosModule = {
    clearDone: function () {
      checkPoint();
      _todos = _todos.filter(function (todo) {
        return !todo.done;
      });
      return todosModule;
    }
    // the rest is the same
  }
}

Now we can just call checkPoint before any action, and we get the undo feature.

var todosModule = {
  add: function (label) {
    checkPoint();
    _todos.push({
      label: label,
      done: false
    });
    return todosModule;
  },
  done: function (index) {
    checkPoint();
    _todos[index].done = true;
    return todosModule;
  },
  clearDone: function () {
    checkPoint();
    _todos = _todos.filter(function (todo) {
      return !todo.done;
    });
    return todosModule;
  },
  toString: function () {
    return _todos.map(function (todo) {
      return todo.label + ' ' + (todo.done ? 'done' : '');
    }).join('\n');
  },
  undo: function () {
    if (_commands.length) {
      _commands.pop().undo();
    }
  }
};

Wait, something is wrong!

We are not correctly restoring the todo list after marking an item done

var todos = Todos();
todos.add('foo').add('bar');
todos.done(0);
console.log(todos.toString());
// foo done
// bar
console.log('undo done()');
todos.undo();
console.log(todos.toString());
// foo done
// bar done

Notice that the item foo still remains done! This is because we are making a copy of the reference to the list, not a deep copy of the todos.

Deep clone _todos

A good approach to simplify the saving and restoring of todos list is to deep clone the list.

function checkPoint() {
  var _ = require('lodash');
  _commands.push({
    undo: function (prevTodos) {
      _todos = prevTodos;
    }.bind(null, _.cloneDeep(_todos))
  });
}

Now any operation can be undone, not matter how it is implemented

var todos = Todos();
todos.add('foo').add('bar');
todos.done(0);
console.log(todos.toString());
// foo done
// bar
console.log('undo done()');
todos.undo();
console.log(todos.toString());
// foo
// bar

I call this approach semi-immutable. We are making a copy of the internal data structure, with this extra complexity inside the Todos instance.

Multiple references and equality

If we reference the todos list in several places, can we check if it has been modified? For example, we could maintain the reference to the list in our MVC model object, and pass reference to the view object.

// model
var todos = Todos();
view.render(todos);
// view renders todos, allows user to mark items done
// should we save todos to the server?

To see if we need to save todos to the server, we could add new feature to the Todos object. Every method that modifies the internal state could mark the object dirty.

function Todos() {
  var _todos = [];
  var _commands = [];
  var _dirty;
  var todosModule = {
    add: function (label) {
      checkPoint();
      _dirty = true;
      // ...
    },
    done: function (index) {
      checkPoint();
      _dirty = true;
      // ...
    },
    clearDone: function () {
      checkPoint();
      _dirty = true;
      ...
    }
    undo: function () {
      if (_commands.length) {
        _commands.pop().undo();
        _dirty = true;
      }
    },
    isDirty: function () {
      return _dirty;
    }
  };

Notice the added complexity: our list of todo items now has two additional features mixed in: undo commands and dirty state bit.

The dirty bit is also inefficient.

var todos = Todos():
save(todos);
todos.add('bar');
todos.undo();
if (todos.isDirty()) {
    save(todos);
}

The second save is unnecessary, since we reverted the state back to the way it was during the first one. Instead of the dirty bit, we could actually compare item contents for equality. For this we need to break privacy and return _todos so that it is possible to compare two lists using deep equality comparison lodash.isEqual.

function Todos() {
  var _todos = [];
  var _commands = [];
  // no dirty bit
  var todosModule = {
    items: function()
    isEqual: function (t) {
        var _ = require('lodash');
        return _.isEqual(t.items(), _todos);
    }
  }
}

We are both breaking the privacy and use expensive method just to confirm that nothing has changed since last time we used the object.

Immutable todo list implementation

The main principle behind the immutable data is that every time something changes we construct a new object. Instead of plain array, I will use a wrapper from seamless-immutable.

var Immutable = require('seamless-immutable');
function Todos(todos) {
  todos = todos || Immutable([]);
  var todosModule = {
    add: function (label) {
      var newTodo = {
        label: label,
        done: false
      };
      var added = todos.concat(newTodo);
      // return new object
      return Todos(added);
    },
    done: function (index) {
      var changed = todos.map(function (todo, k) {
        return k === index ? {
          label: todo.label,
          done: true
        } : todo;
      });
      // return new object
      return Todos(changed);
    },
    toString: function () {
      return todos.map(function (todo, k) {
        return k + ': ' + todo.label + ' ' + (todo.done ? 'done' : '');
      }).join('\n');
    }
  };
  return todosModule;
}

Notice how adding new todo item method changed:

// mutable
function Todos() {
  var _todos = [];
  var todosModule = {
    add: function (label) {
      _todos.push({
        label: label,
        done: false
      });
      return todosModule;
    }
  };
  return todosModule;
}
// immutable
function Todos(todos) {
  todos = todos || Immutable([]);
  var todosModule = {
    add: function (label) {
      var newTodo = {
        label: label,
        done: false
      };
      var added = todos.concat(newTodo);
      return Todos(added);
    }
  };
  return todosModule;
}

We construct new added list by concatenating the existing immutable list with new item, and then call Todos(added) to return completely new object. Each time we call a method on the Todos object, we get back a reference to different object.

var todos = Todos();
var addedFoo = todos.add('foo');
var addedBar = addedFoo.add('bar');
todos === addedFoo; // false
todos === addedBar; // false
addedFoo === addedBar; // false

We need to be careful when using the object because we want to print the object after all modifications

var todos = Todos();
var addedFoo = todos.add('foo');
var addedBar = addedFoo.add('bar');
console.log(todos.toString()); // prints nothing - todos is empty
console.log(addedBar.toString());
// 0: foo done
// 1: bar

Another observation: when setting done property on an object we are constructing new object, thus preventing any modifications through any other possible reference.

Undo command around immutable object

Because each operation on Todos returns new object, we could easily implement the Undo feature using an external proxy object.

function TodosWithUndo() {
  var states = [Todos()];
  var t = {
    add: function (label) {
      states.push(states[states.length - 1].add(label));
      return t;
    },
    done: function (index) {
      states.push(states[states.length - 1].done(index));
      return t;
    },
    undo: function () {
      if (states.length > 1) {
        states.pop();
      }
      return t;
    },
    toString: function () {
      return states[states.length - 1].toString();
    }
  };
  return t;
}

The TodosWithUndo saves the reference to the current immutable

Use:

var ts = TodosWithUndo();
// ts is mutable
ts.add('foo').add('bar');
console.log('after done first');
ts.done(0);
// prints
// 0: foo done
// 1: bar
console.log(ts.toString());
ts.undo();
console.log(ts.toString());
// prints
// 0: foo
// 1: bar

In this implementation, we are sure that every operation creates a new Todo instance, completely separate from the previous one. Nothing in the application logic can modify the list of instances we keep inside TodosWithUndo - not even a stray reference returned to the outside can modify it! The reasoning about program's state and data flow becomes very simple.

Additional questions

Does an object added to the immutable list become immutable?

You can still modify the object via its original reference, but the object inside an immutable collection is a separate copy

'use strict';
var obj = {
  foo: 'foo',
  bar: 'bar'
};
var Immutable = require('seamless-immutable');
var list = Immutable([]).concat(obj);
console.log(list);
// [ { foo: 'foo', bar: 'bar' } ]
obj.foo = 'not foo';
console.log(list);
// [ { foo: 'foo', bar: 'bar' } ]
console.log('object itself', obj);
// object itself { foo: 'not foo', bar: 'bar' }

Is this a deep copy of an object?

Yes, the object is deep cloned when added to the immutable collection

'use strict';
var obj = {
  foo: 'foo',
  bar: {
    baz: 'baz'
  }
};
var Immutable = require('seamless-immutable');
var list = Immutable([]).concat(obj);
console.log(list);
// [ { foo: 'foo', bar: { baz: 'baz' } } ]
obj.bar.baz = 'not baz';
console.log(list);
// [ { foo: 'foo', bar: { baz: 'baz' } } ]
console.log('object itself', obj);
// object itself object itself { foo: 'foo', bar: { baz: 'not baz' } }

Can immutable data structures be used with 3rd party functional libraries, like lodash?

Almost. Lodash can take immutable data structures as inputs for methods that do not modify the input data structure: map, filter, etc. The returned object is NOT an immutable

var _ = require('lodash');
var list = Immutable([1, 2, 3]);
function double(x) { return x * 2; }
var doubled = list.map(double);
console.log(doubled);
// [2, 4, 6]
// doubled[1] = 10; // exception because doubled is Immutable
var _doubled = _.map(list, double);
console.log(_doubled);
// [2, 4, 6]
_doubled[1] = 10; // works because _doubled is a regular Array

A work around is to wrap the returned value in Immutable again

var _doubled = Immutable(_.map(list, double));
// _doubled is Immutable

A convenient library to work with Immutable is Ramda. Just pipe the result through Immutable function call

var R = require('ramda');
var _doubled = R.pipe(R.map(double), Immutable)(list);
console.log(_doubled);
// [2, 4, 6]
// _doubled is immutable too

Are there good libraries for working with immutable data and have large API?

I used seamless-immutable in the above example. It is very simple to use: just wrap an existing object or array to start. Other libraries have richer APIs. Take a look at mori and Immutable.

Isn't deep copy on each operation a slow and memory hungry process?

Yes, if we naively deep cloned everything. Luckily, people have programed the persistent data structures to store diffs efficiently. See this video by David Nolen @swannodette that explains mori implementation performance shortcuts.

Conclusions

Keeping some data immutable (Todos) helped clarify the data flow in this case. At the same time, other pieces can still mutate the state (TodosWithUndo). This way I could dip my toes into the immutable programming paradigm, without rewriting my entire application.