M. Lanza

Swapping Structured Data

Writing algorithms to simulate effects

You’ve seen an atom in an app with a modest bit of infrastructure and a speck of state: a lone integer.

const $counter = $.atom(0);

But a lone integer is not enough for making the kinds of apps people today are interested in using. Let’s take a look at data in a more useful app, one for managing a to-do list.

const data = {
  view: "all",
  next: 1,
  todo: []
}

This is an object literal, as one might define in a script or module. It provides a blank list of to-dos. In many apps, the initial seed of data assumes the user has not yet done work. So no to-dos have been entered or completed.

But to make its meaning more intelligible, let’s see how it looks after a few interactions.

const data = {
  view: "all",
  next: 4,
  todo: [
    {id: 1, title: "Call mom", status: "completed"},
    {id: 2, title: "Buy milk", status: "active"},
    {id: 3, title: "Host movie night", status: "active"}
  ]
}

Seeing this gives you a sense of what the app does and how it works. What remains to be seen is how the app transitions from the initial state to the above.

As before, an atom is that vehicle.

const $state = $.atom({
  view: "all",
  next: 1,
  todo: []
});

It’s seeded with an initial value, a compound data structure which includes properties, a string, an integer and an array—typical state for most apps.

It might’ve been called $todos but since most apps necessitate only a single atom, “$state” suffices. The $state atom keeps data representing the state of the app where it can be managed and monitored, something a variable is ill-equipped to do on its own.

The app needs to update the atom for each discrete action the user takes so that, at any moment, the current iteration of the data structure represents the state of things. Now, as before, although the GUI won’t come until later, one must still anticipate how the app will look and behave.

In this app the user adds to-dos, marks them completed, and toggles between views. To facilitate a user driving the story forward, commands must be written. Let’s start with one for adding a to-do.

This is a function. Recall, that functions can be configurable. For the to-do app to get from its initial state

{
  view: "all",
  next: 1,
  todo: []
}

to the following

{
  view: "all",
  next: 2,
  todo: [
    {id: 1, title: "Call mom", status: "active"}
  ]
}

it needs to take a title argument, making it a configurable, higher-order function in the same vein as the increments function:

function addTodo(title){
  return function(state){
    //To be determined
  }
}

It’s used like so:

$.swap($state, addTodo("Call mom"));
$.swap($state, addTodo("Buy milk"));
$.swap($state, addTodo("Host movie night"));

But, for it to do something, the function needs a body. Let’s compare candidates:

function addTodo(title){
  return function(state){
    const {view, next, todo} = state;
    const id = next,
          status = "active",
          task = {id, title, status};
    state.next++;
    state.todo.push(task);
    //returns original state now modified
    return state;
  }
}
function addTodo(title){
  return function(state){
    const {view, next, todo} = state;
    const id = next,
          status = "active",
          task = {id, title, status};
    //return a new object
    return {
      view,
      next: next + 1,
      todo: todo.concat([task])
    }
}

Though similar, they’re not created equal. Both version destructure the data to access its parts, so no difference there. The first mutates the next and todo properties of the passed-in state object and returns it. The second returns a brand new state object.

So only the second is acceptable.

The term “mutate” is the functional vernacular for change or modify.

Not all data types are capable of being modified—that is, mutated. Strings and integers are replacable, but not modifiable. They’re immutable.

Objects and arrays have slots. Slots, like vars, hold references to other data. That data, too, may have slots. Here data has slots (or properties) for view, next and todo. And todo, being an array, is slotted.

Slots are the basis for compound data structures.

const data = {
  view: "all",
  next: 1,
  todo: []
}

Slots are also the basis for mutability. When a data type has slots, those slots (properties or indexed locations) can be freely replaced by new data. Also, new slots can be added. Thus, slotted data types are mutable and can be changed in place. So although the data constant may not be reassigned, the object assigned to it has slots which can at any time be added, removed, or updated. An indefinite number of to-dos, for example, can be pushed into the todo array.

Consider that a bare minimal primer on mutation. For now, understand that to mitigate the risks associated with shared mutable state, rules must be followed.

Enter the pure function, the centerpiece of functional programming. Your atom’s contents must be strictly swapped using pure functions.

Rules for writing functions:

  1. It must not mutate objects given as arguments
  2. It must access only values/objects given as arguments

A function is “pure” when it abides these rules and “impure” when it doesn’t.

Although both kinds of functions are necessary, separating the pure from the impure parts of a program helps improve its managability and maintainability. You see, much complexity found in programs comes about because of the mutable objects it creates. It becomes difficult to keep tabs on what’s touching what and when.

Programs rely on making changes to the right things in the right order. When done correctly there are no paths whose order hasn’t been considered and tested. If an unanticipated path falls through the cracks, problems arise.

This is the problem functional programming helps solve. That is, a functional core eradicates timing-related concerns and quarantines them to the imperative part of a program. It makes guarantees the imperative part can’t.

Now a pure function does permit mutation. It can mutate any objects which are created inside it up to the moment it returns something. So mutation is not the problem. Mutating state it doesn’t own is. And it doesn’t own what’s passed in. Outside data may be used in other contexts.

Recall how an atom works.

It contains state in a box so that whenever its contents are manipulated (via swap), its subscribers are notified. That there may be more than one potentially makes its contents shared state. Each subscriber relies on trusting the snapshot it last received. Impure swaps which update snapshots violate the trust and, potentially, break things.

An atom sits at the core of a program. Keeping the rules is what makes it a “functional core.” When the atom contained only an integer, as it did in the counter app, this was automatic. An integer is a value type. You couldn’t have mutated it even if you tried. It’s intrinsically immutable.

But objects and arrays, being slotted, are mutable and, thus, must be handled with care. The functions you write must be pure. They return new state without touching passed-in state, as this one does:

function addTodo(title){
  return function(state){
    const {view, next, todo} = state;
    const id = next,
          status = "active",
          task = {id, title, status};
    return { //new return object created
      view,
      next: next + 1, //compute
      todo: todo.concat([task]) //combine
    }
}

While it computes and recombines data, none of the passed-in content is mutated. It is given one frame—a snapshot representing a complete picture of the app—and returns another.

Now, addTodo is just one command. To complete the functional core, other commands, all pure, must be implemented. Each is an algorithm which transforms state from one frame

{
  view: "all",
  next: 1,
  todo: []
}

to the next.

{
  view: "all",
  next: 2,
  todo: [
    {id: 1, title: "Call mom", status: "active"},
  ]
}

Thus, in the early stages of development, the action takes place inside an atom you can peer into and manipulate. The program has no imperative shell. It has no side effects. Let’s be clear that without those things it’s not yet an app.

It’s a simulation.

Consider the typical program. It start with an initial data set and applies computations one after the other to move the story forward, much like our simulation so far.

That said, I’m ready to introduce you to a new perspective on what programs actually are.

ML