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:
- Visual programming - changes of behavior are drawn using the Rosmaro visual editor.
- Functional programming - the whole model is a pure function built out of pure functions and pure data.
First, you draw a graph. Then, you assign functional code to its nodes.
It gives you:
- Automata-based dispatch - actions are dispatched to handlers based on the current node of the state machine. There’s no need to check the current state.
- The right model for the job - the behavior-related state is expressed by a state machine, while the data-related state lives in a dictionary.
- Existing tooling - it’s easy to use with redux and redux-saga.
Rosmaro models support:
- Node multiplication - a node may be multiplied using a function of the context.
- Reuse and composition - models may be included within other models.
- Lenses - thanks to Ramda lenses the shape and size of your data-related state may be easily adjusted.
- Orthogonal regions - multiple regions may be active at the same time. One of the ways to avoid state explosion.
- Subgraphs - nodes of state machines may contain other state machines.
Examples
- Bunny App a wizard implemented in Rosmaro, React, Redux and Redux-Saga.
- TodoMVC the famous TodoMVC demo app
- bool-less-todo a todo app implemented without boolean values and without variables
Utilities
- rosmaro-snabbdom-starter - a zero configuration Rosmaro Snabbdom starter.
- rosmaro-redux - connects Rosmaro, Redux and Redux-Saga.
- rosmaro-react - connects Rosmaro and React.
- rosmaro-binding-utils - makes writing simple Rosmaro handlers easier.
- rosmaro-tools - CLI tooling for Rosmaro.
- rosmaro-testing-library - testing utilities for Rosmaro.
Blog posts
- A JavaScript framework for functions of state and action
- What did we lose when we moved to Redux?
- Dynamic orthogonal regions in state machines
- Decomposing the TodoMVC app with state diagrams
- An overview of the Rosmaro-TodoMVC app codebase
- Testing the TodoMVC app
- State management in JavaScript: data-related state and behavior-related state
License
Rosmaro is licensed under the MIT license.
An example
Building a Rosmaro model consists of two steps:
- Drawing a state machine graph that describes changes of behavior
- 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.
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:
- leaves
- composites
- dynamic composites
- graphs
- external models
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:
- a graph called
main
- a leaf called
Prince
- a leaf called
Frog
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.
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
.
The composite composes two graphs together.
Subgraph A
has an entry point p
which points at node B
.
Subgraph B
has an entry point p
which points at node A
.
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:
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 least one local node
- any number of arrows
- the required
start
entry point - the special
recent
node - any number custom entry points
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:
handler
- the function actually handling actions.lens
- a factory of a Ramda lens specifying the shape and content of the context available for thehandler
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': {/* ... */},
};