Basilisk and Immutability

Jun 14, 2012

For the last year or two I've been working with more and more "app"-like environments: both as actual Apps (ie. Splitsies) but also with complex Flash/Actionscript (at MOO) and of course Javascript. All of these environments have tools for dealing with changes and their propagation: Objective-C has KVO, Actionscript has Bindings, and Javascript has a variety of libraries to do this sort of thing.

And in a vast set of cases, those things map well to what you want to do: have an object, have one or more views of that object, update object, enjoy your Observer Synchronisation.

Wonderful.

Until your model doesn't make sense unless you change two properties "at once"? What does it mean to change Joe's address, element by element? Neither datastructures nor ladies should be observed while changing. In the database world we have ACID, and it helps enormously - but in plain-old-programming we're often out of luck.

One approach which I've found to be incredibly powerful is to make your datastructures immutable, but to make it easy to derive new versions from old ones. I started doing this after watching Rich Hickey's (now fairly well known) InfoQ talks: Simple Made Easy and the less catchily named Persistent Data Structures and Managed References. The basic idea, in javascript terms, is this:

var future = function (present) {
    return present.with(changes);
}

Which is to say: the state of the world at any point in time is fixed. Change is, inherently, something that happens after.

Equally importantly, since you can't change the state of the world (except through some agreed upon process on which the "current" state of the world is replaced by some new state), you can pass the state of the world around to absolutely anyone and everyone, and there is no danger of someone changing it underneath you.

Or to describe all this in javascript:

var joe = {
    name: 'Joe',
    address: { road: 'Kingsway', city: 'London' }
};

/* pass Joe to where-ever you want */

var future = function (current, address) {
    return joe.withAddress(address);
}

joe = future(joe, {
    road: 'A road',
    city: 'New York'
});

// old references to the way Joe
// was are still perfectly valid.

The idea that state and identity should be kept separate isn't new but that doesn't make it any less useful. What you need is a consistent way to talk about what the current state of some identity is, and a simple way to derive the future from that current state.

Introducing Basilisk

Recently I've been working on a Javascript library to make this easy, called Basilisk. The simplest way to describe it is as the javascript-wearing love-child of Python's Named tuples and Clojure. It is evolving quite quickly, and I promise that I will break anything built on it for the next few weeks as the core stabilises, but its already proving useful at MOO where we're using it for ... no, I can't tell you yet.

First we define our structures: we get a very minimal set of helpers for deriving new versions, and actual immutability of these properties in modern browsers.

Note that this is not a type system: the keys define what properties are "special" on the resulting instances, but validation is considered to be a different problem.

var Person = basilisk.struct({
    name: {},
    address: {}
}), Address = basilisk.struct({
    road: {},
    city: {},
}), initial, current;

Now that we have some structures (Person and Address), we can make a man:

// Now, you can create an instance from an object
// literal.
initial = new Person({
    name: 'Joe',
    address: new Address({
        road: 'Kingsway',
        city: 'London'
    })
});

Woo-hoo! We can't change the man...

// Will fail, and print TypeError to the console.
try {
    initial.name = 'James';
} catch (err) {
    console.log(err);
}

but we can create a new one from the old one:

current = initial.withAddress(new Address({
    road: 'Another road',
    city: 'Another city'
}));

// we now have a new version
console.log(current);
// the old version has NOT changed
console.log(initial);

The withAddress function is automatically created for you: more generally, there is a with_ function which takes an object literal with properties to be "different" in the new version. Two useful things with this:

// if nothing has changed (by ===, internally)
// then with_ returns the original object.
current = initial.with_({ name: 'Joe' });
current === initial;

// and it tries to keep unchanged subtrees identical,
// so
current = initial.withName('Bob');

// True
current !== initial;
// Also true
current.address === initial.address;

So why should you care? Firstly, because (along with some additional tools) this makes it super-easy to have only very clear ways for your objects to actually be changed: lots of the "don't look now, I'm changing" type code simply disappears.

And secondly, because it makes your code much, much more robust: instead of there being lots of changing state spread throughout your code, you get lots of functions such as:

var future = function (current, and, a, few, more, values) {
    return current.with(changes);
}

which are comparatively easy to test (everything they depend on is in the argument list).

This entry was tagged Basilisk and Tech