Skip to content

Instantly share code, notes, and snippets.

@thure
Last active February 6, 2023 14:56
Show Gist options
  • Star 46 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save thure/dcffc30117b9a9800084 to your computer and use it in GitHub Desktop.
Save thure/dcffc30117b9a9800084 to your computer and use it in GitHub Desktop.
SCXML Tutorials

Fundamentals: why state machines?

States. The final frontier. These are the voyages of an enterprising developer. Her eternal mission: to explore strange new techniques, to seek out better ways to engineer for mental models and new design patterns. To boldly go where a few awesome devs have gone before.

So you’ve found our poignant guide to SCXML and surely you’re wondering “Why should I want to go out of my way to use formal state machines?” or something like that. Hopefully this introduction addresses that kind of question.

An example: Nancy’s RPG

The problem

Nancy Drew is authoring a fantasy RPG, and some of the NPCs are vampires, for example. Vampires are complicated: if they’re alive, you can chop them to smithereens, but eventually they’ll come back; if they’re impaled by a wooden stake, that puts them in torpor; and if they’re alive or in fleshy smithereens or in torpor, and you burn them, they’ll die forever.

She wonders what the best way to represent those states of being are so that it can also scale. This has to run in the browser, so her options are limited. She likes React, but components’ state property is static. For vampires, state could have the values intact, disabled, torpor, or dead, though it’s the transitions between those states that are complex. Other agents will be able to change a vampire’s state, but only certain transitions between those states are valid.

Vampire components could have a changeState method, though for this use-case it becomes a complex dispatcher with one big switch block full of if blocks:

function changeState (action) {
  switch (action) {
    case DMG_THRESHOLD_STAKED:
      if(this.state === 'intact' || this.state === 'disabled'){
        this.state.set('torpor');
      }
      break;
    case DMG_THRESHOLD_DISABLED:
      if(this.state === 'intact') {
        this.state.set('disabled');
      }
      break;
    case HEAL_THRESHOLD_FULL:
      if(this.state === 'disabled' || this.state === 'torpor'){
        this.state.set('intact');
      }
      break;
    case DMG_THRESHOLD_BURNED:
      if(this.state === 'intact' || this.state === 'disabled' || this.state === 'torpor'){
        this.state.set('dead');
      }
      break;
  }
}

This, Nancy decides, (and for the sake of this tutorial,) is ugly. It won’t scale well if different states are introduced, and in general this pattern doesn’t represent the true, complex nature of a character’s abledness. She certainly doesn’t want to deal with dispatch trees like this when she needs to represent many different types of characters and their diverse states.

Enter state machines

A character’s status in general is more accurately represented by nested categories. Are they alive or dead? If they’re alive, can they move? If not, what will rehabilitate them? What kills an angel? A werewolf? An elf?

She decides state machines are the best way to represent these conditions. Since state machines interpret statecharts, and those can be represented both visually and in machine-readable SCXML she’ll have an easier time both designing and developing the different types of characters.

Instead of the ugly dispatch tree, the vampires’ conditions are represented by this SCXML document:

<scxml initial="intact">
  <sate id="alive">
    <state id="intact">
      <transition event="chopped-up" target="disabled"/>
      <transition event="staked" target="torpor"/>
    </state>
    <state id="disabled">
      <transition event="healed" target="intact"/>
    </state>
    <state id="torpor">
      <transition event="un-staked" target="intact"/>
    </state>
    <transition event="burned" target="dead"/>
  </state>
  <state id="dead"/>
</scxml>

This, Nancy feels, is more elegant, scalable, and easier to read, talk about, and modify. It’s also event-driven, which gels well with the rest of the RPG which has, so far, been developed around events.

So what are state machines good for?

At this point you’re probably thinking “Alright, state machines work well for Nancy Drew’s problems, but will they work well for mine?” Good question.

‘State’ is a pretty good name for the phenomena SCXML can describe – when you’re developing software for discrete objects whose behaviors can be described in discrete groups, and those groups change depending on how the object is acted upon or what the object encounters, then you’re looking at an ideal use-case for statecharts.

Users are themselves complicated and stochastic, but they like it when their appliances and applications operate in simple, clearly defined ways. This applies to many facets of those appliances/applications: the way users interact with them, the way they provide feedback to the user, and the tasks they help the user accomplish.

All of that is a pretty abstract description, but you’ll find plenty of examples throughout this guide that should help illustrate how useful state machines can be.

Fundamentals: states & transitions

At the heart of SCXML, we have the <state> and <transition> nodes. It wouldn’t be a statechart without these nodes.

<transition> nodes live inside <state> nodes and describe what it takes to move (‘transition’) to a different state. Every state has an id attribute that defines its name. Every transition has an event attribute and a target attribute that define which event triggers a move to which state, respectively.

Example: a lamp

Let’s say we have a touch lamp. Whenever you touch it, it turns on if it was off and off if it was on.

So we have two states: ‘on’ and ‘off’. We also have one event: ‘touch’. We can organize this into a statechart like so:

<scxml>
  <state id="on">
    <transition event="touch" target="off"/>
  </state>
  <state id="off">
    <transition event="touch" target="on"/>
  </state>
</scxml>

Easy. This happens to be the simplest functioning statechart you can define.

Example: two buttons, one light

Now say we have one of those antique light switches that operates a light with two buttons: one for ‘off’, and one for ‘on’. Pressing ‘off’ while the light is already off does nothing, and the same for the converse.

For the buttons we have two events, ‘switch-off’ and ‘switch-on’ respectively. For the light we have two states, ‘on’ and ‘off’. That looks like this:

<scxml>
  <state id="on">
    <transition event="switch-off" target="off"/>
  </state>
  <state id="off">
    <transition event="switch-on" target="on"/>
  </state>
</scxml>

From a design standpoint, this configuration takes a little more effort on the user’s part because the user has to decide what she intends to do, which determines which button to press. But perhaps this light consumes a lot of energy to turn on, so accidentally turning the light off is intentionally designed to require more thought.

Example: indoor lights with multiple switches

Let’s step up our game. We have a light on the ceiling which can be switched on or off from multiple locations using on/off light switches that are meant to physically represent the light’s state.

For each switch we have two events, ‘switch-off’ and ‘switch-on’. For the light we have two states, ‘on’ and ‘off’.

The way the light is wired, the position of the switches is, confusingly, irrelevant. The light’s state is toggled regardless of the position of either switch. If both switches fire the same events, we have:

<scxml>
  <state id="on">
    <transition event="switch-on" target="off"/>
    <transition event="switch-off" target="off"/>
  </state>
  <state id="off">
    <transition event="switch-on" target="on"/>
    <transition event="switch-off" target="on"/>
  </state>
</scxml>

You can immediately see the design problem here. Why use a switch which has two physical positions that are meant to map to the light’s current state when those positions are totally irrelevant? Why not just use a single power button?

This is one of the great side-effects of using statecharts: interaction design smells become provocatively clear.

Fundamentals: doing with SCXML

Until now, we’ve written SCXML that doesn’t actually do anything. In the real world, when you transition to a new state UI elements need to be hidden or shown, or the appliance needs to sing a song, or the simulated character needs to use a different sprite. This chapter is about doing those things in SCXML.

There are a few nodes that make this happen, and we’ll introduce more later.

<datamodel> & <data>, and <assign> & <script>

The <datamodel> node with its children the <data> nodes are used to define the namespaces the statechart can use to store information and functions. The statechart can then use <assign> nodes to give those namespaces values and <script> nodes to do something with those values.

In this example we’ll focus only on using <assign> and <script> nodes inside <transition> nodes, though later we’ll describe other ways they can be used.

Here’s a statechart that defines how the user can drag and drop an object:

<scxml>

  <datamodel>
    <data id="position" expr="null"/>
    <data id="element"/>
  </datamodel>
  
  <state id="idle">
    <transition event="mousedown" target="dragging">
      <assign location="position" expr="{x: _event.data.x, y: _event.data.y}"/>
      <assign location="element" expr="_event.data.el"/>
    </transition>
  </state>
  
  <state id="dragging">
    <transition event="mouseup" target="idle"/>
    <transition event="mousemove" target="dragging">
      <assign location="position" expr="{x: _event.data.x, y: _event.data.y}"/>
      <script>
        element.style.left = position.x + 'px';
        element.style.top = position.y + 'px';
      </script>
    </transition>
  </state>
  
</scxml>

It’s important to mention that any scripting in SCXML is ECMAScript (a.k.a. JavaScript).

<assign> uses two attributes, location and expr, which define in which name to store what, respectively. All expr attributes have to be expressions in ECMAScript, as if you were writing the right side of position = …. Notice here the expr attribute is used to define the initial value of the "position" <data> node as well as the values of both names in the <assign> nodes.

The SCXML interpreter makes one more namespace available to us, _event. When an event is fired on the interpreter, you can also pass a payload as data (which is accessed through _event.data), so in this case we expect mousedown and mousemove to come with a payload that has two properties, x and y, and we expect mousedown to also include el.

Fundamentals: compound states

You might have noticed in the introduction our example included <state> nodes nested inside of other <state> nodes. A <state> node that has child states is known as a ‘compound state’.

Compound states are immensely useful. They’re a fantastic means of specifying that a group of states are themselves a state out of which a transition is possible. As in the vampire example: it doesn’t matter if the character is intact, in smithereens, or even in torpor; if the character is burned then the character dies.

The tricky bit is this: if the character were reconstituted from the ashes, what state would he be in? Intact? Torpor? If we were to add <transition event="reconstituted" target="alive"/> inside the dead state, it’s not clear.

There are two ways around this problem: always specify non-compound states (‘simple’ states) as transition targets, or specify an initial state for compound states. If you want your project to scale and you don’t have a specific reason not to point to an initial state, we think you should do the latter.

initial or <initial>

The SCXML standard is extremely consistent everywhere but here. Such is the way of a W3C standard, but that’s part of W3C’s strange beauty. You can specify the first sub-state to go to in a compound state in one of two ways: an initial attribute on the compound state, or an <initial> node with a single <transition target="…"> child.

Here are two statecharts for a microwave that use compound states to describe how the microwave behaves when it’s plugged in:

<scxml>

  <state id="unplugged">
    <transition event="plug-in" target="plugged-in"/>
  </state>

  <state id="plugged-in">
    <initial>
      <transition target="idle"/>
    </initial>
    <state id="idle">
      <transition event="start" target="cooking"/>
    </state>
    <state id="cooking">
      <transition event="stop" target="idle"/>
    </state>
    <transition event="unplug" target="unplugged"/>
  </state>
  
</scxml>
<scxml>

  <state id="unplugged">
    <transition event="plug-in" target="plugged-in"/>
  </state>

  <state id="plugged-in" initial="idle">
    <state id="idle">
      <transition event="start" target="cooking"/>
    </state>
    <state id="cooking">
      <transition event="stop" target="idle"/>
    </state>
    <transition event="unplug" target="unplugged"/>
  </state>
  
</scxml>

Naturally you wouldn’t want a microwave that’s lost power to just keep cooking the moment it’s plugged in again, so we’ve specified "idle" as the state to go to once it’s plugged in.

The initial attribute occupies less space and is arguably more straightforward, especially since you’re not actually allowed to do anything else with the <transition> inside <initial>, but as long as you only use one of these two syntaxes you can decide for yourself which to use.

Using initial or <initial>, you can specify compound interactions that have a default starting point. Later, we’ll introduce a few other ways to jump to a state inside a compound state too, but now you have the tools you need to create a huge range of state machines.

Intermediate topics: conditional transitions

By this point you probably understand generally how statecharts work. In this section we’ll discuss less ubiquitous features of SCXML that nonetheless will come in handy in certain circumstances.

Occasionally a state machine needs to react to an event differently depending on the situation. Are all of the criteria met to transition to this state, or do we need to transition to that state instead?

Let’s consider a situation where we want the logic for a traffic light to be entirely handled by a statechart. The only event the interpreter will receive is "tick", given at an evenly but arbitrarily spaced interval of time. When the interpreter hears a tick, it may need to change the lights, but it may need to keep its present configuration. How do we decide?

Enter cond

The cond attribute lets us specify an expression that the statechart has to evaluate to determine which <transition> to follow. Using cond, we can set up the traffic lights’ statechart this way:

<scxml>
  
  <datamodel>
    <data id="interval" expr="0"/>
    <data id="stop_t" expr="6"/>
    <data id="warn_t" expr="2"/>
    <data id="go_t" expr="8"/>
  </datamodel>
  
  <state id="stop">
    <transition event="tick" cond="interval < stop_t" target="stop">
      <assign location="interval" expr="interval + 1" />
    </transition>
    <transition event="tick" cond="interval == stop_t" target="go">
      <assign location="interval" expr="0" />
      <script>
        _event.lights.set('go');
      </script>
    </transition>
  </state>
  
  <state id="warn">
    <transition event="tick" cond="interval < warn_t" target="warn">
      <assign location="interval" expr="interval + 1" />
    </transition>
    <transition event="tick" cond="interval == warn_t" target="stop">
      <assign location="interval" expr="0" />
      <script>
        _event.lights.set('stop');
      </script>
    </transition>
  </state>
  
  <state id="go">
    <transition event="tick" cond="interval < go_t" target="go">
      <assign location="interval" expr="interval + 1" />
    </transition>
    <transition event="tick" cond="interval == go_t" target="warn">
      <assign location="interval" expr="0" />
      <script>
        _event.lights.set('warn');
      </script>
    </transition>
  </state>
  
</scxml>

While it isn’t time to change lights, we transition to the same state after adding 1 to interval. This is totally legal and a useful pattern in situations where it’s still useful to react to the event without transitioning to a new state. The earlier example in 1.3 used this same pattern to update the position of the element while dragging was still underway.

Using cond, we give the statechart all of the logic pertaining to changing the lights, and it’s only up to the interpreter’s environment to keep track of time by firing "tick" on some interval.

Intermediate topics: the history state

The initial attribute/node is great for specifying a default internal state inside a compound state, but it falls short of one major scenario: resuming where it left off.

We have a lot of interaction scenarios where users essentially pause and resume the task at hand. Games, appliances, and UIs all often have this type of behavior, and it even manifests similarly.

Imagine we have a simple photo gallery app with three views: gallery (all-up), carousel (one-up), and a sharing modal where users can share the album at any time.

<history>, a link to the past

Since the app should return to whichever view it was in before the user invoked the modal, initial states aren’t enough. Instead, when the modal closes, we point to a history ‘state’, which is essentially a shortcut to the substate that was most recently active. That statechart looks like this:

<scxml initial="modal-closed">
  <state id="modal-closed" initial="gallery">
    <state id="gallery">…</state>
    <state id="carousel">…</state>
    <history id="reentry"/>
    <transition event="open-modal" target="reentry"/>
  </state>
  <state id="modal-open">
    <transition event="close-modal" target="reentry"/>
  </state>
</scxml>

Since the interpreter always begins in the gallery, <history> will point there unless the user visited the carousel most recently.

Intermediate topics: the parallel state

Sometimes several stateful processes need to happen simultaneously. There are at least two important approaches to consider:

  1. Make two separate state machines that run concurrently.
  2. Use parallel states in a single state machine.

There are different reasons to choose either technique, and both have their own benefits and obstacles. Should the parallel processes be able to communicate? Are these processes truly independent in all cases? Should there just be multiple instances of the same state machine?

If the processes might need to communicate at some point, if there might be useful states outside of the parallel processes, and/or if the parallel processes aren’t just clones, it would be fruitful to consider using the parallel state so the same state machine governs those processes.

Parallel states are interesting for a few reasons. They’re effectively states, with a target-able id, but they can’t have initial states or attributes because, when entered, the interpreter then steps into every child state of the parallel state, effectively starting multiple slightly independent state machines.

Let’s consider a scenario where the <parallel> node is invaluable: a bifurcated UI.

Navigation & content

Lots of apps have a navigation component, but if the navigation is not monolithic and the content region of the viewport has its own states independent of navigation region’s, then it becomes a smell to create a <state> for each possible combination.

Instead, we can use <parallel>. Let’s create a news app with a content component that animates between views (thus preventing scrolling) and a nav that can expand to show a larger menu or search results. First, we’ll consider each component’s states on their own.

Navigation

The navigation will have a collapsed and an expanded view, since all of our top-level (L1) links will have sub-links. The nav expands when any of the following happen: the user reaches the end of the page, the user swipes down at the top of the page, or the user navigates to an L1. The nav collapses when any of the following happen: the user scrolls the nav out of view, or the user clicks the same L1 they're on.

<scxml initial="nav-closed">

  <datamodel>
    <data id="current-l1"/>
  </datamodel>

  <state id="nav-closed">
    <transition event="click-l1" target="nav-open"/>
    <transition event="scroll-end" target="nav-open"/>
    <transition event="top-swipe" target="nav-open"/>
  </state>

  <state id="nav-open">
    <transition event="click-l1" cond="_event.data != current-l1" target="nav-closed"/>
    <transition event="menu-off-canvas" target="nav-open"/>
  </state>

</scxml>

Content

The content region of the canvas will either freely scroll, or it will stop listening to scroll events and crossfade between pages. The transition here is not dictated entirely by interaction – if the app detects the user intends to navigate, a transition to the animation state is triggered, and only once that animation is complete does the app resume responding to scroll events.

<scxml initial="animating">

  <state id="idle">
    <transition event="navigate" target="animating"/>
  </state>

  <state id="animating">
    <transition event="done-antimating" target="idle"/>
  </state>

</scxml>

Bringing it together

Let’s add one more detail – before the nav or the content are ready, the app needs to make an HTTP request, so there’s a (hopefully) brief loading state.

Combining all three of those components, we have:

<scxml initial="loading">

  <datamodel>
    <data id="current-l1"/>
  </datamodel>

  <state id="loading">
    <transition event="loaded" target="ready"/>
  </state>

  <parallel id="ready">

    <state id="content" initial="animating">

      <state id="content-idle">
        <transition event="navigate" target="content-animating"/>
      </state>

      <state id="content-animating">
        <transition event="done-antimating" target="content-idle"/>
      </state>

    </state>

    <state id="nav" initial="nav-closed">

      <state id="nav-closed">
        <transition event="click-l1" target="nav-open"/>
        <transition event="scroll-end" target="nav-open"/>
        <transition event="top-swipe" target="nav-open"/>
      </state>

      <state id="nav-open">
        <transition event="click-l1" cond="_event.data != current-l1" target="nav-closed"/>
        <transition event="menu-off-canvas" target="nav-open"/>
      </state>

    </state>

  </parallel>

</scxml>

Once the interpreter hears the loaded event, it transitions to the parallel state, which has as children <state> nodes that represent the two separate stateful components of our app. Once there, when the app fires events at the interpreter, both of the children of the parallel state can respond.

@seyedmmousavi
Copy link

Thank you. very clear.
Can you tell us about problems of using State Machines for controlling app logic?
Can I use it for regular applications?
Is it causes to development timeline becomes long and costly?

@sdaves
Copy link

sdaves commented Mar 1, 2018

Thank you for this!
Do you have an idea of the rest of the scope you'd like to cover?

@nurettin
Copy link

nurettin commented Jul 5, 2018

This is great. Qt scxml documentation has a few examples, too.

@jbeard4
Copy link

jbeard4 commented Oct 8, 2018

If anyone is interested, I posted an interactive version of these tutorials here: http://scion.scxml.io/tutorials/fundamentals/

Thanks!

@ykoehler
Copy link

Regarding the history example, I do not see how the first state ever can transition to the modal-open state.

@onacit
Copy link

onacit commented Apr 1, 2019

Type alert!!! <sate .

@sverweij
Copy link

sverweij commented Nov 4, 2019

Nice introduction @thure

The initial state of the content state in the Bringing it together should probably be content-animating a.o.t animating?

    <state id="content" initial="content-animating"> <!-- was: animating -->

      <state id="content-idle">
        <transition event="navigate" target="content-animating"/>
      </state>

      <state id="content-animating">
        <transition event="done-antimating" target="content-idle"/>
      </state>

    </state>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment