Managing States via Prototypal Inheritance

It’s been quite few years I’ve mentioned now and then a prototypal inheritance based pattern to handle generic objects or components state.

Every time I mention such pattern, developers look at me confused, since they’ve never read, thought about, or used prototypes to handle states, and it probably makes little sense to them.

It’s the end of the year, and I feel like I should finally talk about this pattern that’s “so difficult to understand“ … it literally fits in a tweet!

let State=s=>O.assign({},s),O=Object,p=O.getPrototypeOf
State['next']=(a,b)=>O.setPrototypeOf(S(b),a)
State.prev=s=>(s=p(s),s==p({})?null:s)

— Andrea Giammarchi (@WebReflection) December 22, 2016

… and in case you are guessing, the reason I wrote State['next'] in there, is that otherwise Twitter would think it’s a link, penalizing my chars count (how dare they)!

Wait … What ?

Please bear with me, and let me explain after a readable ES5.1 compatible example:

function State(setup/* optional */) { 'use strict';
    Object.assign(this, setup);
}
State.next = function next(state, setup/* optional */) {
    return Object.setPrototypeOf(new State(setup), state);
};
State.prev = function prev(state) {
    var previous = Object.getPrototypeOf(state);
    return previous === State.prototype ? null : previous;
};

Here few very simple concepts introduced by that simple State constructor:

Is it still not so clear? OK, let’s have a practical example:

// imagine a time traveler capable state !!!
let now = new State({time: Date.now()});
let watch = setInterval(
  () => now = State.next(now, {time: Date.now()}),
  1000
);

// or ... imagine just a person ...
class Person {
  constructor() {
    this.state = new State({
        age: 0,
        name: ''
    });
  }
  updateState(data) {
    this.state = State.next(this.state, data);
    // eventually notify a state update
  }
  birthday() {
    this.updateState({
      age: this.state.age + 1
    });
  }
}

// ... that needs to register as citizen ...
class Municipality extends Bureaucracy {
  register(person, name) {
    return new Promise((res, rej) => {
      if (/^\S[\S ]*?\S/.test(name)) {
        super
          .register(person, name)
          .then(res, rej)
        ;
      } else {
        rej(new Error(`Invalid name: ${name}`));
      }
    });
  }
}

// so, imagine me at age 0 !
const me = new Person();

// born in Ancona, Italy
const Ancona = new Municipality();

// registering for the first time
Ancona
  .register(me, 'Andrea')
  .then((name) => {
    me.updateState({name});
  })
  .catch(console.error)
;

How to retrieve all keys from a state ?

That’s a good question, and the answer is straight forward:

State.keys = function keys(state) {
  var keys = [], k;
  for (k in state) keys.push(k);
  return keys;
};

Above solution will probably make some old style linter affectionate’s eyes bleed.

All I can say is that there is absolutely nothing wrong with that logic, but they could go on moaning forever, so here it goes the slowed down, for no reason whatsoever, alternative:

State.keys = function keys(state) {
  var keys = [], set;
  while (state !== State.prototype) {
    // use Reflect.ownKeys if you think you need it
    keys.push.apply(keys, Object.keys(state));
    state = Object.getPrototypeOf(state);
  }
  return Object.keys(
    keys.reduce(
      function (o, key) {
        o[key] = 1;
        return o;
      },
      {}
    )
  );
};

Happy now ?

OK, but how can I diff changes ?

The only difference between an updated state and its previous one, is the amount of own properties.

Basically, all you need to diff between two adjacent states is Object.keys(updatedState), which will inevitably reveal only properties that have been updated.

If these two states are batched, meaning there are intermediate states in between, it’s still quite straight forward to diff properties meaningful for an update.

// how to know what to update ?
function diff(prev, curr) {
  const keys = [];
  while (curr !== prev) {
    keys.push.apply(keys, Object.keys(curr));
    curr = Object.getPrototypeOf(curr);
  }
  return new Set(keys).values();
}

// let's try it
var state = new State();
state = State.next(state, {one:1});
state = State.next(state, {two:2});
var two = state;
state = State.next(state, {three:3});
state = State.next(state, {four:4});
state = State.next(state, {five:5});
var five = state;

// is it really that simple ?
for (let change of diff(two, five)) {
  console.log(change);
}

// logged out:
// five
// four
// three

What about immutability ?

If you want it, you can either create your own version:

function State(setup/* optional */) { 'use strict';
    Object.freeze(Object.assign(this, setup));
}

or you could overwrite next method only to return a frozen object.

State.next = (function (next) {
  return function () {
    Object.freeze(next.apply(this, arguments));
  };
}(State.next));

Funny I am suggesting to mutate a method to have immutability … anyway …

As Summary

Using prototypal inheritance to solve in a different way states chainability seems worth for various use cases and also performance reasons.

Where this solution shines:

Where this solution fails

The second point could have a simple helper, such:

// returns the proto chain length
State.size = function history(state) {
  var i = 0;
  do {
    state = Object.getPrototypeOf(state);
  } while (state !== State.prototype && ++i);
  return i;
};

// flats out a chain
State.merge = function merge(state) {
  return new State(
    State.keys(state).reduce(
      (o, key) {
        o[key] = state[key];
        return o;
      },
      {}
    )
  );
};

// from time to time ..
if (State.size(state) > 512) {
  // reset history
  state = State.merge(state);
}

As you can see, the proposed solution is so simple, anyone can improve it in all aspects.

I hope I’ve finally managed to describe and expose it properly

For those who can, have great holidays.

For those who cannot, respect and thanks for your hard work!

See you next year!

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor