Data-Oriented programming in JavaScript

To Immutability and Beyond




Yehonathan Sharvit viebel

👋Who am I?

  • 💻 Developer since 2001 (JavaScript since 2009, Clojure since 2012)

  • 🎁️Maintainer of Klipse

  • 📖 Author of Data-Oriented programming

  • 📝 Blogger at blog.klipse.tech

  • 👷Software architect at Cycognito (we are hiring)

dop book

Agenda

  1. Complexity of information systems

  2. Principles of Data-Oriented programming

  3. Various ways to achieve data immutability in JavaScript

📓 What is complexity?

⚙️Computational complexity

The amount of machine resources (e.g. CPU, memory) required to run a program.

😰 System Complexity

The amount of brain resources required to understand a system.

📊 Information systems

Systems that manipulate real-word information and activity.




Examples of information systems:

  • Web services that fetch data from data sources and serves it as JSON

  • Web workers that listen to events and enrich event data from multiple data sources

  • Front end applications that communicate with a back end

💡 What is Data-Oriented programming?

A programming paradigm aimed at
reducing complexity of information systems by
treating data as a first-class citizen ️



Principles

  1. Separate code from data

  2. Represent data with generic data structures

  3. Do not mutate data

Why immutability in general?

  • Avoid hidden side effects

  • Thread safety without locks

  • Simpler state management

    • Optimistic locking

    • Time travel

    • Work on a snapshot

  • Values never change

    • Fast equality check

  • Testability

    • Easier to test results than side-effects

    • Order of tests might have an impact on test results

  • Development time

    • Reproducibility

🤔 Do we need immutability in Node.js?

  • JavaScript is single threaded

  • Services are usually stateless

🧀 Who moved my data?

A member of an online library

var kelly = {
  email: "kelly@doe.com",
}

Data enrichment

var additionalData = {
  email: "kelly2@doe.com"
};
updatedKelly = Object.assign(kelly, additionalData)

What happened to kelly?

kelly.email

Concurrency management

var kelly = {
  email: "kelly@doe.com",
  details: {
    preferences: {
      theme: "dark"
    }
  }
}
setBackgroundColor("white");

var a = 1000*(1 + Math.random());
var b = 1000*(1 + Math.random());

setTimeout(() => {
setBackgroundColor(kelly.details.preferences.theme + "blue");
                 }, a);

setTimeout(() => {
  kelly.details.preferences.theme = "light";
}, b);

[a,b]

Watchmen
Seven Habits of Highly Effective People

How to achieve immutability in JavaScript?

The fundamental theorem of JavaScript:

We can solve any problem in JavaScript with a library

and a couple of changes to ECMAScript

Deep clone

The old LISP way!

function deepClone(x) {
  return JSON.parse(JSON.stringify(x));
}
var kelly = {
  email: "kelly@doe.com",
  details: {
    books: 0,
    preferences: {
      theme: "dark"
    }
  },
  history: [/*huge array*/]
}

Let’s clone!

var updatedKelly = deepClone(kelly);
updatedKelly.details.preferences.theme = "light";

kelly is untouched

kelly.details.preferences.theme

⚠️Performance issues: Memory consumption, CPU consumption

Structural sharing

The fundamental theorem of data immutability:
It is safe to share immutable data.


share kelly
var kelly = {
  email: "kelly@doe.com", history: [/*huge array*/],
  details: {
    books: 0, preferences: {
      theme: "dark", notifications: true
    }
  }
}
var updatedKelly = {
  ...kelly,
  details: {
    ...kelly.details,
    preferences: {
      ...kelly.details.preferences,
      theme: "light"
    }
  }
}

Dynamic structural sharing with Lodash FP

Original version

var kelly = {
  email: "kelly@doe.com",
  history: [/*huge array*/],
  details: {
    books: 0,
    preferences: {
                  theme: "dark",
                  notifications: true
                  }
  }
}

Create new version

var updatedKelly = _fp.set(kelly,
                       ["details", "preferences",
                       "theme"],
                       "light")
updatedKelly.details.preferences.theme

Original version is untouched

kelly.details.preferences.theme

Data in common is shared

updatedKelly.history == kelly.history

⚠️Mutations could happen by accident

⚠️Not IDE/TypeScript friendly

🧊 Freeze

function deepFreeze(o) {
  const propNames = Object.getOwnPropertyNames(o);
  for (const name of propNames) {
    const value = o[name];
    if (value && typeof value === "object") {
      deepFreeze(value);
    }
  }
  return Object.freeze(o);
}
var kelly = {
  email: "kelly@doe.com",
  details: {
    books: 0,
    preferences: {
      theme: "dark"
    }
  },
  history: [/*huge array*/]
}
var frozenKelly = deepFreeze(kelly);

In non-strict mode

frozenKelly.details.preferences.theme = "light";
frozenKelly.details.preferences.theme

In strict mode

"use strict"
frozenKelly.details.preferences.theme = "light";
frozenKelly.details.preferences.theme

⚠️We need to remember to freeze

Type friendly structural sharing with Immer

Original version

var kelly = {
  email: "kelly@doe.com",
  history: [/*huge array*/],
  details: {
    books: 0,
    preferences: {
                  theme: "dark",
                  notifications: true
                  }
  }
}

Create new version

updatedKelly = immer.produce(kelly, draftKelly => {
draftKelly.details.preferences.theme = "light";
})
updatedKelly.details.preferences.theme

Original version is untouched

kelly.details.preferences.theme

New version is frozen

"use strict";
updatedKelly.details.preferences.theme = "dark";

Two kind of maps

Records

{
  email: "kelly@doe.com",
  history: [/*huge array*/],
  details: {
    books: 0,
    preferences: {
       theme: "dark",
       notifications: true
    }
  }
}

Associative arrays

{
"user-123": {...},
"user-124": {...},
"user-125": {...},
...
}

A bit of JavaScript history

The limits of the tricks

Structural sharing doesn’t scale with associative arrays

Shallow copying becomes expensive

Welcome HAMT and Immutable.js

Beyond Infinity: Advanced structural sharing

persistent vector



var brown = Immutable.List([0, 1, 2, 3, 4, 5, 6, 7, 8])



var blue = brown.set(5, "beef")

Advanced structural sharing with Immutable.js

A big native object

var bigMap = Array.from(Array(10000).keys()).reduce(function(res, i)  {
  res["hello-" + i] = Math.random(); return res;
}, {});
Object.keys(bigMap).length

A big immutable map

var bigMapImmutable = Immutable.fromJS(bigMap);

Perfs on shallow copying

benchmark(10, function() {
  updatedMap = {...bigMap, "hello-10": 10};
})

Perfs of advanced structural sharing

benchmark(10000, function() {
  updatedMapImmutable = bigMapImmutable.set("hello-10", 10);
})

⚠️CPU consumption during conversion to native Objects

Proxies again

var kelly = {
  email: "kelly@doe.com",
  history: [/*huge array*/],
  details: {
    books: 0,
    preferences: {
      theme: "dark",
      notifications: true
    }
  }
}
var users = {"kelly" : kelly};
var immutableUsers = Immutable.fromJS(users);
immutableUsers["kelly"].details.preferences.theme

Fake it…​

function fake(c) {
  return new Proxy(c, {
    get(target, property, receiver) {
      var value = target.get(property);
      if (Immutable.isImmutable(value)) {
        return fake(value);
      }
      return value;
    }
  });
}

... until you make it

fake(immutableUsers)["kelly"].details.preferences.theme

Native JavaScript immutable objects

var kelly = #{
  email: "kelly@doe.com",
  history: #[/*huge array*/],
  details: #{
    books: 0,
    preferences: #{
       theme: "dark",
       notifications: true
    }
  }
}
var updatedKelly = #{
  ...kelly,
  details: #{
    ...kelly.details,
    preferences: #{
      ...kelly.details.preferences,
      theme: "light"
    }
  }
}

Equality is defined by value

assert(#{ a: 1 } === #{ a: 1 });
assert(#[1, 2] === #[1, 2]);

Data cannot me accidentally mutated

kelly.details.preferences.theme = "dark";
/* throws an exception! */

Summary

Various ways to achieve immutability:

  • Spread operator

  • Lodash FP

  • Immer

  • Immutable.js

  • JavaScript Records

Parameters to consider:

  • State management

  • Associative arrays or records

dop book