Consider the following example:
function add(x, y) { return x + y; }
var collection = Object.freeze([1, 2, 3, 4]);
var consumerA = collection; // expects steady data
var consumerB = collection; // requires the latest data
var updatedCollection = collection.concat(5);
consumerA.reduce(add, 0); // 10 (desired result)
consumerB.reduce(add, 0); // 10 (incorrect result, should be 15)
consumerA
operates with the immutable data it expects. What can be done in Javascript to ensure that consumerB
always accesses the latest data?
Please notice: Just deep copying consumerA
and treating collection
as mutable data isn't an option.
UPDATE: The example merely serves to illustrate the fundamental problem, which is caused by shared reference types: Some consumers (or reference holder) rely on immutable, others on mutable data. I'm looking for a proper change tracking mechanism that solves this problem without undermining the benefits of immutable data.
Maybe the term "change tracking" is too vague. With change tracking I mean a way for consumerB
to be informed about the change (push mechanism) or (more interesting) to be able to discover the change (pull mechanism). The latter would require that consumerB
somehow gets access to the updated collection.
Well, that's my only solution, but there are probably others. I wrap my immutable collection in a mutable object. A consumer which needs constant data holds a reference to the collection itself. A consumer which requires current state holds a reference to the wrapper. I use a primitive form of structural sharing in order to avoid cloning:
function add(x, y) { return x + y; }
var collection = Object.freeze([1, 2, 3, 4]);
var atom = {state: collection};
var consumerA = collection;
var consumerB = atom;
console.log(consumerA === consumerB.state); // true (obviously)
// naive structural sharing to avoid cloning
atom.state = Object.create(atom.state, {length: {value: atom.state.length, writable: true}});
atom.state.push(5);
Object.freeze(atom.state);
// as desired
console.log(consumerA.reduce(add, 0)); // 10
console.log(consumerB.state.reduce(add, 0)); // 15
// structural sharing is used
console.log(Object.getPrototypeOf(consumerB.state) === collection); // true
// object comparison simply by reference check
console.log(consumerA === consumerB.state); // false
By wrapping an immutable collection in a mutable wrapper it becomes a kind of persistent data type. That means it can be treated as a normal, mutable object but leaves its previous versions untouched, hence persistent. By the way, to name the wrapper atom
isn't an accident, but a reference to the corresponding data type in Clojure.
Please note: To use the prototype system for structural sharing can lead to memory leaking and should be used with caution only.