Use an Elm program as a state machine for a JS/Node application (experimental)
The Elm architecture is composed of
- a model (a statically typed data structure)
- an update (a function that handles updates to the model's values)
- and a view (a template that renders HTML with the current model values as inputs).
visotype/state-machine is
- a package for Node (and web application bundlers)
- that lets you store your data model in an Elm program
- and update its values through a JS-Elm interop
- with some built-in type safety features.
This approach gives you some of the benefits of Elm in catching type errors at compile time but does not require you write your entire application in Elm. It enables you to
- use other templating languages (like Pug or Handlebars) for HTML markup
- render simple templates without virtual-dom diffing
- use JavaScript to implement intended side effects triggered by model updates
- interact with your data model through a promise API.
You might be interested in this approach if you prefer the simplicity of the Elm architecture to component-based architectures like React Redux, but you find it cumbersome to write view templates and handle user inputs in Elm.
You might not like this approach if you think that type-safety guarantees for inputs to and outputs from your data model are not enough, and you would prefer that all of your application logic gets checked by the Elm compiler.
const { initialize, updateKey } = require('state-machine');
(async () => {
try {
const model0 = {
a: 1,
b: [2, 3],
c: { x: 'hello', y: 'world' },
};
const machine = await initialize(model0);
const model1 = await updateKey(machine, 'a', '(+)', 1);
const model2 = await updateKey(machine, 'b', 'Array.push.int', 4);
const model3 = await updateKey(machine, 'c', 'Dict.union', { y: 'universe' });
console.log([model0, model1, model2, model3]);
} catch (error) {
console.log(error.message);
}
})();
Logged output:
[ { a: 1, b: [ 2, 3 ], c: { x: 'hello', y: 'world' } },
{ a: 2, b: [ 2, 3 ], c: { x: 'hello', y: 'world' } },
{ a: 2, b: [ 2, 3, 4 ], c: { x: 'hello', y: 'world' } },
{ a: 2, b: [ 2, 3, 4 ], c: { x: 'hello', y: 'universe' } } ]
Returns a promise for a state machine. The actual return value is an object that provides an interface to a compiled Elm program via message-passing functions.
model
Type: object
The state machine's initial model. The initial model must contain all of the
keys that your application will use. Each key can have a different JavaScript
value type, but a number
must remain a number
, an array
must remain an
array
, and so on.
Returns a promise for the state machine's current model.
machine
Type: object(function)
A state machine that has been initialized with a model.
Returns a promise for the value of a key the state machine's current model.
machine
Type: object(function)
A state machine that has been initialized with a model.
key
Type: string
A key in the state machine's model.
Returns a promise for the state machine's updated model.
machine
Type: object(function)
A state machine that has been initialized with a model.
key
Type: string
A key in the state machine's model.
f
Type: string
The name of an Elm function to apply to the value at the selected key. The
module name should be included, as in 'String.length'
or 'List.append'
, but
may be omitted for functions in the Basics module like 'negate'
or 'round'
.
Operators should be enclosed in parentheses like '(+)'
or '(::)'
. Only
functions that return the same type as the selected key are allowed. Some
functions are only allowed when an extension is provided to specify expected
types, as in 'always.string'
or 'Array.push.int'
. See below for a full list
of allowed functions.
args
Arguments to f. Types must correspond to the Elm function's type signature. The selected key's value is appended as the last (rightmost) argument. Will return a rejected promise if there are too many or too few arguments or type decoding fails.
Documentation for Elm core library functions can be found
here. The following
function names are allowed as arguments to updateKey
:
'(+)'
or'Basics.(+)'
'(-)'
or'Basics.(-)'
'(*)'
or'Basics.(*)'
'(/)'
or'Basics.(/)'
'(//)'
or'Basics.(//)'
'(^)'
or'Basics.(^)'
'round'
or'Basics.round'
'floor'
or'Basics.floor'
'ceiling'
or'Basics.ceiling'
'truncate'
or'Basics.truncate'
'not'
or'Basics.not'
'(&&)'
or'Basics.(&&)'
'(||)'
or'Basics.(||)'
'xor'
or'Basics.(xor)'
'modby'
or'Basics.modby'
'remainderBy'
or'Basics.remainderBy'
'negate'
or'Basics.negate'
'abs'
or'Basics.abs'
'clamp'
or'Basics.clamp'
'sqrt'
or'Basics.sqrt'
'logBase'
or'Basics.logBase'
'degrees'
or'Basics.degrees'
'radians'
or'Basics.radians'
'turns'
or'Basics.turns'
'cos'
or'Basics.cos'
'sin'
or'Basics.sin'
'tan'
or'Basics.tan'
'acos'
or'Basics.acos'
'asin'
or'Basics.sin'
'atan'
or'Basics.atan'
'atan2'
or'Basics.atan2'
'identity'
or'Basics.identity'
'always.string'
or'Basics.always.string'
'always.char'
or'Basics.always.char'
'always.int'
or'Basics.always.int'
'always.float'
or'Basics.always.float'
'always.list'
or'Basics.always.list'
'always.array'
or'Basics.always.array'
'always.dict'
or'Basics.always.dict'
'Array.set.string'
'Array.set.char'
'Array.set.int'
'Array.set.float'
'Array.push.string'
'Array.push.char'
'Array.push.int'
'Array.push.float'
'Array.append.string'
'Array.append.char'
'Array.append.int'
'Array.append.float'
'Array.slice'
'Bitwise.and'
'Bitwise.or'
'Bitwise.xor'
'Bitwise.complement'
'Bitwise.shiftLeftBy'
'Bitwise.shiftRightBy'
'Bitwise.shiftRightZfBy'
'Char.toUpper'
'Char.toLower'
'Char.toLocaleUpper'
'Char.toLocaleLower'
'Dict.insert'
'Dict.remove'
'Dict.union'
'Dict.intersect'
'Dict.diff'
'(::)'
or'List.(::)'
'List.reverse'
'List.append'
'List.intersperse'
'List.tail'
'List.take'
'List.drop'
'Set.insert.string'
'Set.insert.char'
'Set.insert.int'
'Set.insert.float'
'Set.remove.string'
'Set.remove.char'
'Set.remove.int'
'Set.remove.float'
'Set.union.string'
'Set.union.char'
'Set.union.int'
'Set.union.float'
'Set.intersect.string'
'Set.intersect.char'
'Set.intersect.int'
'Set.intersect.float'
'Set.diff.string'
'Set.diff.char'
'Set.diff.int'
'Set.diff.float'
'String.reverse'
'String.repeat'
'String.replace'
'String.append'
'String.slice'
'String.left'
'String.right'
'String.dropLeft'
'String.dropRight'
'String.cons'
'String.toUpper'
'String.toLower'
'String.pad'
'String.padLeft'
'String.padRight'
'String.trim'
'String.trimLeft'
'String.trimRight'