3

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.

  • Make `consumerB` a deep copy of `collection`, then update `consumerB`. – Cerbrus Nov 25 '15 at 15:19
  • @Cerbrus read the question. – Arg0n Nov 25 '15 at 15:20
  • The code behaves as I'd expect immutable types to behave. Because its immutable, `collection.concat(5)` must not modify the collection, it must return a new collection with the added element. Why would you expect `consumerB` to contain the added element? It's a reference to the original collection. –  Nov 25 '15 at 15:25
  • Can the collection contain objects? And are you concerned about changes to individual objects in the collection? – lintmouse Nov 25 '15 at 16:09
  • @Arg0n: I did. The OP only mentioned making `collection` mutable isn't an option. – Cerbrus Nov 25 '15 at 17:36
  • @Amy: I don't expect that. My updated question should answer yours –  Nov 25 '15 at 19:43

2 Answers2

3

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)); 
R3tep
  • 12,512
  • 10
  • 48
  • 75
  • Yeah, I want to avoid deep copying altogether. I'm looking for a proper change tracking mechanism. –  Nov 25 '15 at 15:30
  • @IvenMarquardt: That's not possible. You can't have immutable objects _and_ mutable objects without deep copying. What you want can't be done. – Cerbrus Nov 25 '15 at 17:37
  • @Cerbrus: I know what you mean. To update immutable data you always have to copy it. That's excatly what I'm doing with `collection.concat`. That's not what I mean. I rephrase my question accordingly. –  Nov 25 '15 at 18:14
0

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.