Skip to content

Instantly share code, notes, and snippets.

@mogsie
Created January 12, 2018 11:33
Show Gist options
  • Save mogsie/529de70de9e36776e97cd56296798fb1 to your computer and use it in GitHub Desktop.
Save mogsie/529de70de9e36776e97cd56296798fb1 to your computer and use it in GitHub Desktop.

I'm wondering if it would be useful to provide a way to have guards be kept out of the state machine, to keep it all "pure function".

The way I thought it might work would be to add a parameter to the transition to specify the value of any guards. The guards would always be optional, but would of course influence the outcome. Here's a machine with a named guard (I use 'guard' instead of 'cond' because I like statechart terminology better than scxml):

m = Machine({
  foo: {
    on: {
      A: bar,
      B: {
        target: bar
        guard: isEmpty
      }
    }
  },
  bar: {}
})

var object = []

direct = transition(m.initialState, 'A');  // transitions directly to bar, as before
guarded = transition(m.initialState, 'A');  // about to transition from foo to bar, but it was guarded.
guarded.missingGuards; // [ "isEmpty" ]
failure = transition(m.initialState, 'A', { isEmpty: false }); // state is still "foo" — the event was basically not handled.
success = transition(m.initialState, 'A', { isEmpty: true }); // state is "bar" as the guard allowed the transition to happen.

A successful state transition requires that you verify that missingGuards is empty. If there are any missingGuards then you need to retry the state transition providing those missing guards. The missingGuards would be a property on the State object returned.

The third parameter to transition would be somewhat of a bitset with the names of the guards and their boolean values.

The guard attribute should allow boolean logic, though, so that it's possible to say guard: "!isEmpty && ready" in the machine definition, and that this would require the isEmpty and ready guards to be provided.

@davidkpiano
Copy link

So, historically, some statechart implementations have the notion of a "junction" state, and I think we can introduce something like that. I say that because:

B: {
  target: bar
  guard: isEmpty
}

With the above, it's hard to represent multiple conditions. I think we can keep the existing syntax, with a couple changes:

B: {
  bar: { cond: 'isEmpty' },
  baz: { cond: 'isValid' },
  other: { cond: true } // represents if all other conditions are false
}
  1. cond can be a string, just like actions
  2. cond: true (or something similar?) can represent the "fallback" condition (sink state? not sure if those semantics apply here)

And to support this, we'd introduce a JunctionState:

JunctionState {
  conditions: {
    isEmpty: State { value: 'bar', ... },
    isValid: State { value: 'baz', ... },
    default: State { value: 'other', ... }
  }
}

The above is very much subject for discussion... maybe we can somehow represent the "default" state inside the conditions object, maybe we should rename the conditions object, etc.

What do you think?

@mogsie
Copy link
Author

mogsie commented Jan 13, 2018

My main point of this gist was the API for pure functional guards, i.e. that if a guarded transition was found, that the response was a new State object with no actions, but with information about which guards needed to be provided, instead of injecting code into the statechart JSON object (which is a lot harder to serialize and so on).

But I agree, the syntax for guards wasn't well thought through. As I mentioned in the github issue, with the introduction of guards, an event might have two or more transitions attached to them. The ordering of the eavluations of those guards is important, The author of the state machine puts them in the order they make sense; the first one that evaluates to true is fired, the rest aren't evaluated.

So some sort of array syntax would be preferable.

Perhaps:

A: 'bar', // old syntax for unguarded
B: [
  { isEmpty: 'bar' },
  { isValid: 'baz' },
  'other'   // represents if all other conditions are false
]

or

A: 'bar', // old syntax for unguarded
B: [
  { target:'bar', cond: 'isEmpty' },
  { target:'baz', cond: 'isValid' },
  'other'   // represents if all other conditions are false
]

With this it is also possible to envision:

{ 'isEmpty && isValid' : 'foo' },
// or
{ target:'foo', cond: 'isEmpty && isValid' },

A plain old C-style boolean logic support with the guarded booleans.

@davidkpiano
Copy link

I'm in favor of the boolean logic strings - it's otherwise difficult to represent boolean expressions with data structures in JSON (although if we could find a way, that would be nice).

I'm still worried about arrays, because it makes this possible:

B: [
  { target: 'bar', cond: 'isEmpty' },
  { target: 'bar', cond: 'somethingElse' },
  { target: 'foo', cond: 'isEmpty' }
]

Two problems with the above - 'foo' and 'bar' are both "selected" (I know 'bar' would win but ideally order shouldn't matter) and there are two branches that both lead to 'bar'. This makes it harder to statically analyze, instead of just merging both conditions.

@mogsie
Copy link
Author

mogsie commented Jan 13, 2018

Well, order does matter, and I feel that's a rather contrived example.

It's a bit like saying that boolean logic i flawed because you might write stupid stuff like if (isEmpty && somethingElse && isEmpty) — Yes, the last isEmpty is redundant :).

These guards are the if tests of statecharts, and we should spend time getting them right, that's for sure :)

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