Skip to content

Instantly share code, notes, and snippets.

@mogsie
Created December 5, 2017 21:43
Show Gist options
  • Save mogsie/c0948bd3da58ecc88507db4e162a4bba to your computer and use it in GitHub Desktop.
Save mogsie/c0948bd3da58ecc88507db4e162a4bba to your computer and use it in GitHub Desktop.

I thought I'd weigh in on how I see guards in "stateless" statecharts They're totally deterministic, given the same evaluations of the guards.

When I write tests for my state machines, I set it in an initial state, I send it a sequence of events, and ensure that the guards evaluate to certain values. The state machine simulates timeouts so it runs through all the states almost instantaneously. When it's done, I "expect" a particular side effect to have happened.

So for example, given a simple state machine with states X and Y, and the "foo" event makes it do something:

 X          foo                 Y
           ------>           entry / dosomething
                             exit / stopdoingsometing

I would start the state machine, fire the "foo" event, and then the test would check that the "dosomething" function was called, not that it's in the Y state.

Likewise, if there's a guard, e.g. foo / isReady — then I could test that the doSomething action is only executed if the test sets up isReady to be true.

This type of testing / structuring statecharts allows you to refactor the statechart, e.g. introduce substates and so on. For example, Here Y is moved into a substate, and transitions to Y2 on the timeout element (presumably because an action happens in Y or Y2 (not shown)).

 X          foo          +------- Z ------------------------+
           ------>       |  entry / dosomething             |
                         |  exit / stopdoingsometing        |
                         |                                  |
                         |                                  |
                         |      Y    ---------->    Y2      |
                         |                timeout           |
                         |                                  |
                         +----------------------------------+

The test still works, and it would even work if the entry/exit actions were moved around from Z to Y, since we don't verify which state the machine entered, simply the side effects of executing the machine.

What the statemachine guarantees (and what I like to test) is that

  • Given an uninitialized state machine (start of the state machine)
  • And a set of events (or simulated timeouts, e.g. "after 1s")
  • And a set of guard evaluations (at each event)
  • Then a specific set of side effects will happen (actions)
@davidkpiano
Copy link

davidkpiano commented Dec 5, 2017

That was my thinking too - to have "side effects" be declarative as well. Here's the syntax I'm thinking of:

function logEntry(message) {
  return console.log('Entry: ' + message);
}

function logExit(message) {
  return console.log('Exit: ' + message);
}

const lightMachine = Machine({
  key: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: {
          // guard: action == TIMER and extended timer state is between 1000 and 2000
          greenYellow: ({ timer }) => timer >= 1000 && timer < 2000,
          yellow: ({ timer }) => timer >= 2000
        }
      },
      onEntry: logEntry,
      onExit: logExit
    },
    greenYellow: {
      onEntry: logEntry,
      onExit: logExit
    }
  }
});

const nextState = lightMachine.transition('green', 'TIMER', { timer: 1500 });
// State {
//   value: 'greenYellow',
//   previous: 'green',
//   entry: logEntry, // called with value
//   exit: logExit // called with previous
// }

// test assertions
assert.equal(nextState.value, 'greenYellow');
assert.equal(nextState.entry, logEntry);
assert.equal(nextState.exit, logExit);

// execute side effects
// this is the only side-effectful function in xstate and is not required
// the developer can execute the entry/exit functions themselves
Machine.exec(nextState);
// => 'Exit: green'
// => 'Entry: greenYellow'

@mogsie
Copy link
Author

mogsie commented Dec 5, 2017

Ok, here's how I might approach this. I'm just doodling, so please bear with me :)

// side effect free boolean synchronous functions
var guards = {
  isReady: function() {
    // inspect the world and return true or false;
    return true;
  }
}

const my = Machine({
  key: 'light',
  initial: 'X',
  states: {
    X: {
      on: {
        foo: 'Z'
      },
    },
    Z: {
      onEntry: 'startSomething', // could also be function references, but this would be serializable
      onExit: 'stopSomething',
      states: {
        Y: {
          on: {
            otherevent: {
              // event       [ guard ]   ------> target
              // otherevent [ isReady ]   ------> Y2
              isReady: Y2,
            }
          }
        },
        Y2: {
        }
      }
    }
  }
});

// The guards are actually called to be able to evaluate what to do.
const nextState = my.event('foo', guards);
// State {
//   value: ['Z','Y'],
//   previous: ['X'],
//   actions: ['startSomething']
// }

// I could execute side effects, but in a testing scenario I wouldn't have any 
// side effects, only assert that the right actions are called given the events passed in.

const nextState2 = my.event('otherevent', guards);
// State {
//   value: ['Z','Y2'],
//   previous: ['Z', 'Y'],
//   actions: []
// }

@davidkpiano
Copy link

I like the idea of function references and, as long as they're named functions, they should be serializable (just by parsing as fn.name). With your approach, guards is external, which implies that you can dynamically have other guards, which doesn't seem like a valid use-case (that is, if the statechart is well-defined and deterministic, guards wouldn't have to ever be dynamic, and can be "known" when the statechart is defined).

What would .event() represent?

@mogsie
Copy link
Author

mogsie commented Dec 6, 2017

event() is my way of throwing an event at the state machine, passing in the name of the event (foo then otherevent). The reason I pass in the guards is to avoid the guard function references to be part of the statechart code (like in SCION-CORE). The reason I call them events is it reflects more the name of the real-world happening rather than the state-machine side effect (a transition). Also, in a given state, sending an event may have no effect; a transition may or may not take place; and it is essentially of no consequence if it did or not; the consequences we're interested in are the actions to perform.

The reason I feel guards should be external is that the statechart needs to evaluate the guards in order to determine which of the transitions to pick, given an event.

Why is it important for the state machine to be deterministic? In my view, the point of a statechart is to define a clear behaviour (actions) in the face of a varying set of events with a varying set of guardd conditions. For example, in my statechart, it is expected that the machine does nothing (no action) when I pass the otherevent event, if the isready guard is false. The way I see it that's pretty deterministic.

In a real usage of the statechart, the guard would be backed by code that inspected the world, e.g. checked that an array had enough data to continue, or checked that some variable had an appropriate value. Those would be implementation details when it comes to the statechart.

Another reason for passing in the guards like this is to ease testing. To test a certain behaviour, you just supply the value of all the guards the statechart can assess, for each transition (or event).

The API between the state machine and the rest of the world then boils down to:

  • the complete set of guards it evaluates
  • the complete set of events it understands
  • the complete set of actions it responds with

Importantly, as I mentioned before, I consider the actual state names to be unimportant to the user of the state machine.

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