sebmarkbage / ecmascript-immutable-data-structures

600 stars 16 forks source link

Immutable all the way #3

Open Gozala opened 9 years ago

Gozala commented 9 years ago

I don't see any note in the document whether those immutable data structures can contain mutable items in them or not. I would personally advocate against allowing that since:

  1. That would make a lot of otherwise possible optimization impossible.
  2. If it's immutable all the way there is a possibility of cheap transfer of them over workers (or why not even sharing).
andersekdahl commented 9 years ago

I think this is very important for this proposal because - like you said - making it immutable all the way enables transfer/sharing between threads, and that's a huge win. But also because allowing mutable data inside immutable data seems like a pretty big foot gun.

With immutable all the way, you can't put functions inside immutable structures (since functions are objects that are mutable). But it would be really cool if there were immutable functions as well, which when executed returns a promise for when it was really executed. That way, you could share it with another thread, and when the other thread calls it, the real function is added to the event loop of the original thread. Something like:

// thread A
let z = 1;

const map = #{
  x: 1, 
  y: 2,
  doStuff: ImmutableFunction(() => {
    z++;
    return z;
  });
};

shareWithThreadB(map);

// thread B

self.onShare = function (map) {
  console.log(map.get('x'));
  // Calling map.doStuff() adds to the event loop of thread A to actually invoke
  // the real function
  map.doStuff().then(z => console.log(z));
}

It would be necessary to make sure that arguments passed to an immutable function could only be immutable data, and that they could only return immutable data. Or that any mutable data passed/returned would be automatically copied to an immutable object.

If it's not immutable all the way, it raises some questions about value equality. If two immutable maps contain an object each, would comparing the immutable maps compare the objects inside them by reference equality or value equality? If by value equality, how would that work?

With immutable data all the way, you can be sure that value equality can be somewhat cheap since you can compare a hash of the data (the hash still has to be computed, of course), but if there's mutable data inside it you have to make a deep equality check.

AsaAyers commented 9 years ago

Associating mutable data with points in an immutable structure can be done with Symbols and WeakMaps. (I think Symbols are already immutable)

let asa = { name: "Asa", favoriteColor: "blue" }

const weak = new WeakMap();
const users = #{
  asa: Symbol('asa')
}
weak.set(users.asa, asa)

It might be slightly awkward that you have to pass the two around together

let asa = weak.get(users.asa)
asa.favoriteColor = "green"

weak.get(users.asa).favoriteColor // 'green'
AsaAyers commented 9 years ago

If immutable structures can hold mutable data then it seems like they would require deep comparison to really know if they are equal or not.

function memoize(fn) {
    const memo = new WeakMap()
    return function(object) {
        if (memo.has(object)) {
            // nothing has changed
            return memo.get(object)
        }
        const ret = fn(object);
        memo.set(object, ret);
        return ret;
    }
}

const doubleFooValue = memoize(function(object) {
    return object.foo.value * 2;
});

const immutable = #{ foo: { value: 2} }
doubleFooValue(immutable) // 4
immutable.foo.value = 11
doubleFooValue(immutable) // wrong result: 4

edit: if you remove the # when defining immutable that code runs as-is in Chrome and gives the exact same results.

andersekdahl commented 9 years ago

@AsaAyers Symbols themselves are immutable in that you cannot assign new properties on them like:

var mySymbol = Symbol('test');
mySymbol.x = 'z';
console.log(mySymbol.x); // logs 'undefined'

But all symbols share the same prototype, and that's not an immutable object. So you can still do:

Symbol.prototype.data = {x: 'z'};
var mySymbol = Symbol('test');
console.log(mySymbol.data.x); // logs 'z'
mySymbol.data.x = 'y';
console.log(Symbol.prototype.data.x); // logs 'y'

Not sure if that's an issue for transferability/shareability though, since the global prototypes on different threads would be different in any case, and sending a Symbol over threads would hopefully give that Symbol that threads global Symbol prototype. But I can imagine that this has some issues.

AsaAyers commented 9 years ago

2ality has some great info on Symbols and it mentions passing Symbols across realms (I think realms === threads for this purpose).

I also decided to try some experiments in the console with that page open. The things I found unexpected were that while Symbol.prototype exists, the instances don't have a prototype. They also all inherit from the local Symbol.prototype even if it originated in another realm.

local = Symbol('hello')
// Symbol(hello)
remote = frames[0].Symbol('hello')
// Symbol(hello)
Object.getPrototypeOf(local)
// VM1181:2 Uncaught TypeError: Object.getPrototypeOf called on non-object
//     at Function.getPrototypeOf (native)
//     at <anonymous>:2:8
//     at Object.InjectedScript._evaluateOn (<anonymous>:895:140)
//     at Object.InjectedScript._evaluateAndWrap (<anonymous>:828:34)
//     at Object.InjectedScript.evaluate (<anonymous>:694:21)(anonymous function) @ VM1181:2InjectedScript._evaluateOn @ VM149:895InjectedScript._evaluateAndWrap @ VM149:828InjectedScript.evaluate @ VM149:694
Object.getPrototypeOf(remote)
// VM1182:2 Uncaught TypeError: Object.getPrototypeOf called on non-object
//     at Function.getPrototypeOf (native)
//     at <anonymous>:2:8
//     at Object.InjectedScript._evaluateOn (<anonymous>:895:140)
//     at Object.InjectedScript._evaluateAndWrap (<anonymous>:828:34)
//     at Object.InjectedScript.evaluate (<anonymous>:694:21)(anonymous function) @ VM1182:2InjectedScript._evaluateOn @ VM149:895InjectedScript._evaluateAndWrap @ VM149:828InjectedScript.evaluate @ VM149:694
Symbol.prototype.hello = "Hello World"
// "Hello World"
local.hello
// "Hello World"
remote.hello
// "Hello World"
local.prototype
// undefined
remote.prototype
// undefined