arqex / freezer

A tree data structure that emits events on updates, even if the modification is triggered by one of the leaves, making it easier to think in a reactive way.
MIT License
1.28k stars 56 forks source link

Not purely immutable? #45

Closed MichaelOstermann closed 8 years ago

MichaelOstermann commented 8 years ago

Hi!

I noticed the fact that when you update a tree, it automatically updates itself. So for example:

Mutable:

var example = {};

// We can mutate this from anywhere - bad, right?
example.foo = 'bar';

console.log(example.foo); // 'bar'

Freezer:

var example = new Freezer({});

// We can still mutate this from anywhere in the code...
example.get().set('foo', 'bar');

console.log(example.get().foo); // 'bar'

This way the original source can be mutated exactly as before from anywhere in the code, causing the side-effects which immutable data structures are supposed to circumvent.

In my particular use case I wanted to take an existing tree A and create another tree B somewhere else that takes A and adds some additional properties - but now A has my additional properties and fires events.

Is this intended, or can this be turned off somehow?

Thanks!

arqex commented 8 years ago

Hi @MichaelOstermann

TLDR; Yes and no ( worst tldr ever, you'll need to keep reading )

Freezer is intended to contain your app state. The data that a freezer object keeps is immutable, and it is impossible to change:

var freezer = new Freezer({
    people: [
        {name: 'John', age: 22}
    ]
});

// The data is immutable
var person = freezer.get().people[0];

person.age = 40;
console.log( person.age ); // 22

person.set({ age: 40 });
console.log( person.age ); // 22

But you probably want your app state to be updated at some time ( especially if you want your app to do something ). Using the updater methods in the nodes you can replace the state that freezer contains by another immutable data structure.

var state = freezer.get();
state.people[0].set({ age: 40 });

console.log( state.people[0].age ); // 22
console.log( state == freezer.get() ); // false
console.log( Freezer.get().people[0].age ); // 40

In your application you use the data that freezer contains, not the freezer object, so you are using immutable data.

Those side effects that you are remarking are produced by uncontrolled changes in the data you are using. A typical example is showing a function like this:

// f needs to do 2 things with the state
var f = function( state ) {
    f1( state ); // But if the state is modified in f1
    f2( state ); // f2 would have an undesired result
}

// Since freezer uses frozen data, you can be sure there 
// will be no side-effects
f( freezer.get() );

In frontend developing, the function that must never update the data is the render one.

Freezer is great for reactive apps, because even if f1( state ) uses any of the updater methods, the update event is not triggered instantly, but in the next tick, so the function f will end before the new update event is triggered, and you can create a data driven app like the one below and not enter in an infinite loop:

freezer.on('update', function( newState ){
    f( newState );
});

// now every change in the data will trigger our function
freezer.get().people[0].set({name: 'Mary'});

Immutability prevents bad practices like updating the state in the middle of a render, but your app state needs to change over the time. It's not a problem that your state can be updated from anywhere in your app, the problem is not controlling that change. Using freezer, any change in the data will update the whole state and trigger an event, letting your app to update accordingly.

MichaelOstermann commented 8 years ago

Thank you for the thorough explanation, I understand the concept now! :)