Rosmaro

Read Me

Reacting to the same action in different ways due to what happened in the past may be a challenge.

Rosmaro is a framework for writing functions like this:

({state, action}) => ({state, result})

It places great emphasis on two programming paradigms:

First, you draw a graph. Then, you assign functional code to its nodes.

Rosmaro dispatch

It gives you:

Rosmaro models support:

Examples

Utilities

Blog posts

License

Rosmaro is licensed under the MIT license.

An example

Building a Rosmaro model consists of two steps:

  1. Drawing a state machine graph that describes changes of behavior
  2. Writing functional code - pieces of behavior to associate with graph nodes

Let’s build a model of a cursed prince, who turns into a frog after eating a pizza.

Although we could write a JSON file describing the graph by hand, it’s a lot more fun to use the Rosmaro Editor.

model graph

After drawing the graph visible above, the following JSON is generated automatically.

{
  "main": {
    "type": "graph",
    "nodes": {
      "Prince": "Prince",
      "Frog": "Frog"
    },
    "arrows": {
      "Prince": {
        "ate a pizza": {
          "target": "Frog",
          "entryPoint": "start"
        }
      }
    },
    "entryPoints": {
      "start": {
        "target": "Prince",
        "entryPoint": "start"
      }
    }
  },
  "Prince": {
    "type": "leaf"
  },
  "Frog": {
    "type": "leaf"
  }
}

The graph tells the story of how does the model change over time and what makes it change. At the beginning it behaves like a Prince. That’s what the arrow from the start entry point pointing at the Prince node is telling us. Then, as soon as the Prince eats a pizza, he follows the arrow called ate a pizza. The main graph is not anymore in the Prince state, but in the Frog state.

Now it’s the time to code different behaviors.

Hint: writing simple handlers is a lot easier with the rosmaro-binding-utils package! Take a look how does this example look written using rosmaro-binding-utils.

This is the behavior of the Frog:

const Frog = ({action, context}) => {
  switch (action.type) {
    case 'INTRODUCE_YOURSELF':
      return {result: "Ribbit! Ribbit!", arrows: [], context};
    default:
      return {result: undefined, arrows: [], context};
  }
};

It reacts to only one action type - INTRODUCE_YOURSELF. Every single time when it’s asked to introduce itself, it makes the Ribbit! sound.

This is the behavior of the Prince:

const Prince = ({action, context, node}) => {
  switch (action.type) {
    case 'INTRODUCE_YOURSELF':
      return {result: "I am The Prince of Rosmaro!", arrows: [], context};
    case 'EAT':
      const arrows = action.dish === 'pizza'
        ? [[[node.id, 'ate a pizza']]]
        : [];
      return {result: undefined, arrows, context};
    default:
      return {result: undefined, arrows: [], context};
  }
};

Every time he introduces himself, he says I am The Prince of Rosmaro!. When he eats a pizza, he follows the ate a pizza arrow.

The only missing piece is the behavior of the main node, that is the parent of both Prince and Frog nodes. It simply passed the action to its only active child:

const main = ({children, action}) => Object.values(children)[0]({action});

Now it’s time to associate code with nodes:

const bindings = {
  'main': {handler: main},
  'main:Prince': {handler: Prince},
  'main:Frog': {handler: Frog},
};

Please notice how bindings are named. The Prince handler is assigned to the main:Prince node, because Prince is the child of main.

Let’s put it all together!

import rosmaro from 'rosmaro';

const graph = {
  "main": {
    "type": "graph",
    "nodes": {
      "Prince": "Prince",
      "Frog": "Frog"
    },
    "arrows": {
      "Prince": {
        "ate a pizza": {
          "target": "Frog",
          "entryPoint": "start"
        }
      }
    },
    "entryPoints": {
      "start": {
        "target": "Prince",
        "entryPoint": "start"
      }
    }
  },
  "Prince": {
    "type": "leaf"
  },
  "Frog": {
    "type": "leaf"
  }
};

const main = ({children, action}) => Object.values(children)[0]({action});

const Frog = ({action, context}) => {
  switch (action.type) {
    case 'INTRODUCE_YOURSELF':
      return {result: "Ribbit! Ribbit!", arrows: [], context};
    default:
      return {result: undefined, arrows: [], context};
  }
};

const Prince = ({action, context, node}) => {
  switch (action.type) {
    case 'INTRODUCE_YOURSELF':
      return {result: "I am The Prince of Rosmaro!", arrows: [], context};
    case 'EAT':
      const arrows = action.dish === 'pizza'
        ? [[[node.id, 'ate a pizza']]]
        : [];
      return {result: undefined, arrows, context};
    default:
      return {result: undefined, arrows: [], context};
  }
};

const bindings = {
  'main': {handler: main},
  'main:Prince': {handler: Prince},
  'main:Frog': {handler: Frog},
};

const model = rosmaro({graph, bindings});

The model function has the following signature: ({state, action}) => ({state, result}).

Here we can see how does it work:

let state;
[
  {type: 'INTRODUCE_YOURSELF'},
  {type: 'EAT', dish: 'yakisoba'},
  {type: 'INTRODUCE_YOURSELF'},
  {type: 'EAT', dish: 'pizza'},
  {type: 'INTRODUCE_YOURSELF'}
].forEach(action => {
  const {state: newState, result} = model({state, action});
  state = newState;
  console.log(result);
});
// I am The Prince of Rosmaro!
// undefined
// I am The Prince of Rosmaro!
// undefined
// Ribbit! Ribbit!

When the model is called with no state, it generates its initial value based on entry points. Every function call gives us the actual result of the call and the new state. The new state is meant to be passed to the model function the next time we want to handle an action.

The code of this example is on GitHub.

Graphs

Introduction

The graph is the most outer layer of a Rosmaro model. It is meant to represent all of the possible changes of behavior.

There are five types of nodes:

The root node of the model graph is the main node, which is mandatory. It may be of any of the supported node types, even a leaf.

Let’s take a look at the example graph one more time. It’s a collection of three nodes:

Both Prince and Frog are leaves. The main graph has two children, which are also called Prince and Frog. Their underlaying nodes are Prince and Frog, respectively.

The Rosmaro Editor

The recommended way of drawing a graph is using the Rosmaro Editor. To get started you don’t need to install anything, as it is available on-line. A new graph may be created, or an existing JSON file may be imported.

However, if you only want, you can actually code parts of the graph or even the whole graph, as the JSON file generated by the editor is meant to be human-readable.

Leaves

Leaves are the basic nodes. They have no children. All they have is a name, so they can be used as underlaying nodes for children of graphs or composites.

Leaves themselves have no visualization as they have no children.

Here’s an example model graph where main is a leaf:

{
  "main": {"type": "leaf"}
}

Entry points are totally ignored when the target is a leaf. It’s because a leaf cannot have different states.

Composites

Composites are a way to introduce orthogonal regions. If there’s a node A and there’s a node B, we can create a composite C with A and B as underlaying nodes. That way if the model is in the C state, it has both the behavior of A and the behavior of B.

This is what a composite with two local nodes (First composed node and Second composed node) looks like.

Composite

The code, assuming there are some nodes A and B which are used as underlaying nodes for First composed node and Second composed node, looks like this.

{
  // some main
  "A": // some node
  "B": // some node
  "C": {
    "type": "composite",
    "nodes": {
      "First composed node": "A",
      "Second composed node": "B"
    }
  }
}

Composites are transparent for entry points. Here’s an example of how does it work.

There’s a graph, where some node follows the action arrow and enter the p entry point of the composite.

arrow to a composite

The composite composes two graphs together.

arrow to a composite

Subgraph A has an entry point p which points at node B.

arrow to a composite

Subgraph B has an entry point p which points at node A.

arrow to a composite

In such a situation, the entered entry point is required to be present in both graph.

To sum it up, the result of main:some node following the action arrow to enter composite through the entry point p is setting the model to (main:composite:subgraph A:B, main:composite:subgraph B:A).

Composites are transparent to arrows followed by their children. If at least one child follows some arrow, the whole composite follows this arrow. If two or more children follow some arrows, the composite follows those few arrows simultaneously.

Dynamic composites

Dynamic composites are very similar to regular composites with one very important difference - the list of children nodes is generated using a function of the context.

Let’s take a look at this regular composite.

{
  "main": {
    "type": "composite",
    "nodes": {
      "First": "Child",
      "Second": "Child"
    }
  },
  "Child": {"type": "leaf"}
}

The main composite has two children: First and Second. They are all leaves defined by the Child node. The number of main’s children never changes.

The same graph structure may be achieved using a dynamic composite.

{
  "main": {
    "type": "dynamicComposite",
    "child": "Child"
  },
  "Child": {"type": "leaf"}
}

The binding of the main node must define a function of the context which returns local names of the children.

{
  'main': {
    nodes: ({context}) => context.myChildren,
    handler: // ...
  }
}

This dynamic composite associated with the binding visible above results in a graph identical to the regular composite described at the beginning of this section, when the context looks like this.

{
  myChildren: ["First", "Second"]
}

However, as soon as a transition changes the context, the graph also changes.

Let’s say the context looks like this.

{
  myChildren: ["First", "Second", "Third"]
}

Then the graph equals the following regular composite.

{
  "main": {
    "type": "composite",
    "nodes": {
      "First": "Child",
      "Second": "Child",
      "Third": "Child"
    }
  },
  "Child": {"type": "leaf"}
}

What’s important to note is that when a dynamically generated child is removed, its current nodes are forgotten. So if there were three dynamic children: First, Second and Third and we removed the Second one, the next time it appears it behaves like it never existed.

Graphs

Graphs are the most important type of nodes. We’re going to discuss them using the following example:

arrow to a composite

Here is the generated JSON file:

{
  "some leaf": {
    "type": "leaf"
  },
  "another leaf": {
    "type": "leaf"
  },
  "main": {
    "type": "graph",
    "nodes": {
      "A": "some leaf",
      "B": "another leaf"
    },
    "arrows": {
      "A": {
        "going for a walk": {
          "target": "B",
          "entryPoint": "start"
        },
        "doing a loop": {
          "target": "A",
          "entryPoint": "twisted"
        }
      },
      "B": {
        "going back": {
          "target": "A",
          "entryPoint": "start"
        }
      }
    },
    "entryPoints": {
      "start": {
        "target": "A",
        "entryPoint": "beginning"
      },
      "history": {
        "target": "recent",
        "entryPoint": "going back to the past"
      },
      "back door": {
        "target": "B",
        "entryPoint": "window"
      }
    }
  }
}

Graphs are made of:

At any given time, there’s just one active graph node.

There are two local nodes in the screen-shot above: A and B. They must be associated with some actual nodes, like leaves, composites or other graphs. It’s done by selecting the underlaying node.

Entry points specify which local node is going to be active once a transition to the graph occurs.

Only one entry point is mandatory and it’s the start entry point. It cannot point at the recent node. Except this one, there may be any number of custom entry points.

An arrow from an entry point to a node means that when the graph is entered through that entry point, the active node is going to be the node the arrow is pointing at. The arrow also specify an entry point. In the picture visible above, the start entry point is pointing at the A node and specifies that it should be entered through the beginning entry point point. Arrows may be pointing just from entry points and never at entry points. Also, there may be just one arrow from one entry point.

There’s one special node and it’s the recent node. It symbolizes the last node which was active, before the graph was left. If the graph has never been entered, it’s the node the start entry point is connected to. In the picture above we can see that if the graph is entered through the history entry point, then it’s most recent active child is going to be entered through the going back to the past entry point.

Arrows between nodes symbolize how does the behavior change. Here, when the current node is A and it follows the going for a walk arrow, it changes the current node of the graph to B which is entered through the start entry point.

Loops are allowed.

All the nodes visible when drawing a graph are local nodes. They are not available outside the graph that’s being edited. The actual node, which is entered when a local node is entered, is picked using the underlaying node select field.

If a local node follows an arrow, which is not connected to any other local node, the whole graph may follow this arrow. It works very similar to event bubbling. To make it work we need to extend the arrows returned by the handler. Let’s say e leaf follows an arrow like this:

[
  [['main:subgraph:A', 'arrow_found_in_main']]
]

To make the subgraph node follow the arrow_found_in_main arrow if there’s no such arrow within the subgraph itself, the arrow must be extended in the following way:

[
  [['main:subgraph:A', 'arrow_found_in_main'], ['main:subgraph', 'arrow_found_in_main']]
]

External nodes

Every Rosmaro model is built based on a description of a graph and its behavior. It looks like this:

{graph, bindings}

Objects like these may be included within bindings for external nodes. It enables us to reuse once created Rosmaro models.

Here’s an example model graph where main is an external model:

{
  "main": {"type": "external"}
}

Bindings

Introduction

While the graph is just data representing how does the behavior change, bindings represent the actual behavior.

Because a Rosmaro model is a pure function, all bindings must have no side effects.

When an action is passed to the model, it’s redirected to the appropriate handler picked based on the state of the state machine.

Let’s assume that the current node is A (which is a leaf) and that this is its binding:

const A = {
  handler: ({action, context}) => ({
    result: {'A got': action},
    arrows: [],
    context,
  })
};

Then when we call the model in this way:

model({state, action: {type: 'TEST'}})

The result is gong to be something like this:

{result: {'A got': {type: 'TEST'}}, state: /*...*/}

Arrows must always be successfully followed. If an arrow cannot be followed, an error is thrown.

Node bindings

A binding is a node’s behavior specification. It is an object associated with a graph node. It may have up to three parts:

  1. handler - the function actually handling actions.
  2. lens - a factory of a Ramda lens specifying the shape and content of the context available for the handler
  3. nodes - a function of the context returning the list of children. It’s used only by dynamic composites.

The handler is a function meant to consume actions. It has the following signature:

({action, context, node, children}) => ({
  result,
  arrows,
  context
})

The action parameter is the action to handle, like {type: 'SEARCH', what: 'kitties'}.

The context is a model-wide bag of data, shaped by the lens attached to the node and those attached to its ancestors. It’s where the data-related state lives in. By default it’s undefined.

The node is an object which holds the ID of the node the handler is attached to, like main:a:b:c. An example:

{id: 'main:a:b:c'}

The children parameter is an object which keys are names of the children of the node, and values are functions which allow to call them. Let’s say the current node has two children: A and B. Then within the handler we can write code like that:

({action, context, node, children}) => {

  // Calling the child named A.
  const ({
    result: AResult,
    context: AContext,
    arrows: AArrows
  }) = children['A']({action}); // Please notice we pass only the action.

  // Calling the child named B.
  const ({
    result: BResult,
    context: BContext,
    arrows: BArrows
  }) = children['B']({action}); // Please notice we pass only the action.

  return {
    result: somehowMergeResults(AResult, BResult),
    context: somehowMergeContext(AContext, BContext),
    arrows: somehowMergeArrows(AArrows, BArrows),
  };
}

Handlers of nodes which have only one children, like graphs, are also passed a children object. It simply has just one value.

The returned result may be of any type. It’s the result property returned by the model function. The context returned by calling a child is seen through the lens for the parent node. It means that the shape of the context passed to the parent and the contexts returned by the children is the same. If you use this low level API, then it’s up to you to merge the contexts returned by the children into the one passed to the parent handler. Both arrows returned by a child and the arrows meant to be returned by the parent have the following format:

[
  // The first arrow:
  [['main:parent:A', 'x']],
  // The second arrow:
  [['main:parent:B', 'y']],
]

If we want the arrow to “bubble up”, that is the parent node to follow x and y, it needs to be extended this way:

[
  // The first arrow:
  [['main:parent:A', 'x'], ['main:parent', 'x']],
  // The second arrow:
  [['main:parent:B', 'y'], ['main:parent', 'y']],
]

The lens factory is a function which takes the local node name and returns a Ramda lens. Here’s an example of a lens which picks the part of the context denoted by the key named after the local name of the node (like c in the case of main:a:b:c):

import {lensProp} from 'ramda';
// ...

{
  lens: ({localNodeName}) => lensProp(localNodeName),
  // ...
}

The nodes function looks like in the example above:

{
  nodes: ({context}) => context.myChildren,
  // ...
}

It gets the context (as an object property) and returns a list of strings representing the names of children.

Building a model

Introduction

The graph is just pure, immutable data.

The bindings are made of pure functions.

Building a Rosmaro model is how we put them together.

The built model redirects method calls to appropriate handlers based upon the current node of the graph. The current graph node and context of the model are provided as the state parameter.

The Rosmaro factory function comes from the rosmaro package.

$ npm i rosmaro --save

This is how we call it.

import rosmaro from 'rosmaro';

const model = rosmaro({graph, bindings});

Graph

The graph parameter is a parsed JSON file generated using the Rosmaro Editor. It should have the following format:

const graph = {
  'main': {type, /* ... */},
  'another node': {type, /* ... */},
  'yet another node': {type, /* ... */},
  /* ... */
};

What’s important to node is that what the Rosmaro factory expects is actually a plain old JavaScript object. Let’s assume the graph is stored in a file called graph.json. Then we load it like this:

import graph from './graph.json'

Bindings

Bindings define the behavior of nodes. They are assigned to full node names, like this:

const handlers = {
  'main': {/* ... */},
  'main:the_first_child_of_main': {/* ... */},
  'main:the_second_child_of_main': {/* ... */},
  'main:the_second_child_of_main:the_only_child_of_the_second_child_of_main': {/* ... */},
};