Łukasz Makuch

Łukasz Makuch

What did we lose when we moved to Redux?

In simple procedural programming we used IFs to choose between code blocks operating on data structures.

That's how a data structure could look:

let shape = {
  type: 'circle',
  r: 100
}

And this could be a procedure operating on it:

function makeBigger(shape) {
  if (shape.type === 'circle') {
    shape.r = shape.r * 2;
  } else if (shape.type === 'rectangle') {
    shape.a = shape.a * 2;
    shape.b = shape.b * 2;
  }
}

Such a little piece of code was so straightforward that it was possible to understand it even without knowing JavaScript. That was a big plus!

However, experience showed that IFs like the one visible within the body of the makeBigger procedure were repeated multiple times in other areas of the codebase. For instance, in a procedure meant to draw a shape:

function draw(shape) {
  if (shape.type === 'circle') {
    // The logic responsible for drawing a circle.
  } else if (shape.type === 'rectangle') {
    // The logic responsible for drawing a rectangle.
  }
}

This is where object oriented programming came to the rescue. It provided a dispatch mechanism that associated procedures with data structures.

For instance, there could be classes representing circles and rectangles:

class Rectangle {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }

  makeBigger() {
    this.a = this.a * 2;
    this.b = this.b * 2;
  }

  draw() {
    // The logic responsible for drawing a rectangle.
  }
}
class Circle {
  constructor(r) {
    this.r = r;
  }

  makeBigger() {
    this.r = this.r * 2;
  }

  draw() {
    // The logic responsible for drawing a circle.
  }
}

This approach not only made the code more coherent by grouping procedures that were closely related to each other, but also saved us from constantly examining the type of structures we were working with. It meant no more if (type === 'cicle') nor if (type === 'rectangle'). We simply put the code related to circles into the Circle class and the one that was all about rectangles into the Reactangle class. Then it was enought to call someShape.draw() and the proper implementation of draw was selected for us based on the type of someShape. Neat!

But it wasn't all roses. It was soon that we discovered how difficult it was to synchronize local state changes with other state changes and with updating the user interface.

Looking for a way to solve this issue resulted in a shift to more declarative and functional programming.

The popular React Redux architecture defined views in a declarative manner, so that developers didn't need to manually trigger updates of the UI when the state changed. Also, state updates were happening in a more controlled way.

Unfortunately, in terms of function dispatch, Redux didn't offer more than the procedural approach. The subscribe method left us with a data structure we needed to manually inspect in order to select the proper branch of code. It was quite common to see IFs like this one repeated over and over again both in the reducers and in the render function:

if (isFriend) {
  if (hasEaten) {
    // What to do when it's your friend and it has already eaten.
  } else {
    // What to do when it's your friend but it hasn't eaten yet.
  }
} else {
  // What to do when you're not a friend. 
}

Putting great emphasis on declarative views and immutable data structures proved to be beneficial, no doubt about that. Nevertheless, I believe that there are still some valuable techniques we can learn from object oriented programming when it comes to function dispatch. That's especially true when the decision depends not only on the data type, what's usually indicated by some sort of a type* property, but also on events that occurred in the past, which like to hide behind boolean flags such as has* or is*.

I'd like to present a dispatch mechanism that identifies the ability to respond differently to the same action as one of its key purposes. And all that without a single boolean value!

When is it useful? Just think of a RENDER action. It's always the same action, and yet, depending on the actions consumed in the past, we may want it to return different results.

In this system, functions are assigned to nodes of a state machine, what is somehow similar to how methods are associated with types of objects.

The Rosmaro architecture

It's heavily inspired by both the state design pattern we know from object-oriented programming and functional front end architectures, like The Elm Architecture.

The current behavior of a model is defined by the currently active node.

To alter it, the active node returns an arrow describing what has just happened:

PIZZA: () => ({
  result: `Tasty`,
  arrow: `ate`,
}),

It makes the state machine transition to the node this arrow is pointing at.

The control flow reflects the hierarchical structure of the state machine. Parent nodes are capable of changing the way their children are called. They may also modify the result:

PIZZA: alterResult(r => `${r}, my friend!`),

Wrapping the result of calling the child node turns out especially helpful when composing UI components.

Speaking of user interfaces, recently I released a screencast showing the complete process of using automata-based programming to build a wizard application. If you like this form, you may find it a nice introduction to this dispatch mechanism.

From the author of this blog

  • howlong.app - a timesheet built for freelancers, not against them!