Reducers

Reducers are functions given a Model and Actions, implements the update logic of the application, returning a new model the world.

Purity

The reducer must be referentially transparent and a pure function. This is a mouthful to say that, whenever you invoke the function with the same arguments, it produce the same result. This, in practice, means:

  • It must take all the arguments by value or const reference.
  • It must perform no side-effects, this is: no writing or reading to disk, generating non deterministic random numbers, making connections to servers, etc.
  • And of course, no reading or writing mutable globals, statics, singletons, and other evils.

Sadly, C++ has no builtin mechanism to enforce function purity and this depends only on programmer discipline. It is technically possible to perform side effects directly from the reducer, but it is hard to say in general terms how much this messes up with the system. For programs that rely on the reproducibility of the reducer to implement undo or time travel, it can have fatal consequences.

Tip

While the reducer must not perform side effects, it definitelly can, and often should, describe side effects. The Effects section covers this aspect in detail.

Why the name?

Given an action, model types, and a update reducer function (in the Architecture section we built a complete example), we can write a function that takes an initial model, a sequence of actions, and returns the current model after applying all the actions like this:

model update_all(model init, const std::vector<action>& actions)
{
    return std::accumulate(
        actions.begin(), actions.end(),
        init,
        update);
}

The std::accumulate function is called reduce in many other languages, like Python, JavaScript, Scheme or Clojure. It takes a sequence of inputs and a binary operation, and it “reduces” the sequence to a single value, by succesively applying the binary operator to the last output and the next input.

The reducer is the binary operation that we use to reduce a sequence of actions to a single model state—it is the last argument we passed to accumulate.

Note

Reduce, also known as fold, is one of the most important higher-order functions in functional programming, because it provides a general mechanism for performing iterative sequential computations. Transducers are a powerful abstraction based on the idea of transforming reducers.

State machines

As we depart from Object-Oriented Design and abandon UML class diagrams, we find ourselves devoid of modeling tools and visual representations for our system. State machines are a good tool to model Lager based systems, specially components with asynchronicity and transient states. We can follow the following analogies:

model states
actions transitions
reducer transition table
_images/torniquete.jpg

Consider the example of a turnstile. A turnstile, used to control access to subways and amusement park rides, is a gate with three rotating arms at waist height, one across the entryway. Initially the arms are locked, blocking the entry, preventing patrons from passing through. Depositing a coin or token in a slot on the turnstile unlocks the arms, allowing a single customer to push through. After the customer passes through, the arms are locked again until another coin is inserted. This can be modelled by the following state diagram: (source)

_images/turnstile.svg

Such diagram can be systematically translated into Model, Actions and Reducers, so that it can be executed in a Lager application:

#include <lager/util.hpp>

struct locked {};
struct unlocked {};
using model = std::variant<locked, unlocked>;

struct push {};
struct coin {};
using action = std::variant<push, coin>;

model update(model m, action a)
{
    return std::visit(lager::visitor{
        [] (push) { return locked{}; },
        [] (coin) { return unlocked{}; },
    }, a);
}

In this case, the model was so simple that we only needed to pattern match the action. In more complicated cases we might need to analize the state inside the action (or otherwise) to fully implement the transition table.