user5536315
user5536315

Reputation:

How to track changes with shared, immutable reference types in Javascript

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.

Upvotes: 3

Views: 422

Answers (2)

user5536315
user5536315

Reputation:

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.

Upvotes: 0

R3tep
R3tep

Reputation: 12864

You use Object.freeze when you declare collection, so you can't added properties to collection.

When you create consumerB, you execute a copy of the object collection

var consumerB = collection; 

So you can't added properties to consumerB like collection.

You need to clone the object instead copy it. You can do it like :

var consumerB = JSON.parse(JSON.stringify(collection)); 

Upvotes: 3

Related Questions