Closed joshhornby closed 6 years ago
The problem with guards as part of the state machine is that:
(Math.random() <= 0.5)
)With that said, due to the binary nature of guards (true/false), and the idea that one would map arbitrary events (such as a button click) to discrete actions, guards simply become abstracted away:
function getClickAction() {
const num = Math.random();
let nextAction;
// guard
if (num <= 0.5) {
nextAction = 'FOO';
} else {
nextAction = 'BAR';
}
return nextAction;
}
Then, you can have:
function handleClick() {
// pretend there's a currentState and machine in outer scope
currentState = machine.transition(currentState, getClickAction());
}
And still be fully deterministic, with guards.
With that said, there's definitely some ideas I have floating around about modeling decision trees in conjunction with hierarchical finite state machines. I just wish that JavaScript had a decent pattern matching abstraction (switch/case
is inadequate) that ensures exhaustiveness. I think there's a proposal for that somewhere.
Okay, I've reconsidered guards. Here's what they'd look like:
const lightMachine = Machine({
key: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: {
state: 'yellow',
guard: fullState => fullState.elapsed >= 30000
}
}
},
yellow: {
on: {
TIMER: {
state: 'red',
guard: fullState => fullState.elapsed >= 5000
}
}
},
red: {
on: {
TIMER: {
state: 'green',
guard: fullState => fullState.elapsed >= 30000
}
}
}
}
});
lightMachine.transition('green', 'TIMER', { elapsed: 10000 }).value;
// => 'green'
lightMachine.transition('green', 'TIMER', { elapsed: 30001 }).value;
// => 'yellow'
Eventually, I'd like to have a deterministic + declarative way of:
s.elapsed >= 10
and s.elapsed <= 20
would overlap in two separate guards)This is all possible because even though it might seem like guard conditions introduce the idea of "infinite" actions (e.g., TIMER + elapsed: 10
, TIMER + elapsed: 11
, etc.), it can still be classified as a finite set of actions per state transition. In the above example, for the out-bound green
transition, actions can be classified into:
TIMER
where s.elapsed >= 30000
TIMER
where s.elapsed < 30000
(complement of above guard condition)so it's essentially just two actions.
Since we're providing the key
to the machine, we can probably simplify the API too:
// xstate knows to read from the 'light' property because
// { key: 'light' } is specified in the machine definition
lightMachine.transition({ light: 'green', elapsed: 30001 }, 'TIMER');
// => 'yellow'
Yay or nay?
I think this should be called conditions instead of guards to be more in line with the state chart literature.
Also I think they should use natural language instead of functions. Then when you build the machine you should pass the state chart plus implementations of all the conditions.
A lot of time conditions in a state chart are complex policies which could have different implementations in different environments (e.g. simulation vs in product)
@camwest What would that look like in the API, do you think? I'm all for the natural language approach, especially if these are meant to be consumed in different environments/languages.
Here is my revised version of what guards (and onEntry/onExit
hooks) would look like:
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'
I think this can easily be adapted to accept a string key
for the guard, and then have the guard implementation able to be referenced in the second argument to Machine(config, options)
.
What do you think?
I think it's useful for names to be explicitly assigned to the guards. In certain domains these will be well known policy names.
For example. In Constructing the User Interface with Statecharts there is a CD Player example. The conditions (guards) they mention are "No CD in drawer", "CD in drawer", "end of CD".
const machine = generateMachine(statechart, (state, is, on) => {
on("startup", () => cdPlayer.closeDrawer());
state("NO CD Loaded", () => {
state("CD Drawer Closed", () => {
on("Eject", () => cdPlayer.openDrawer());
});
state("CD Drawer Open", () => {
on("Eject", () => cdPlayer.closeDrawer());
});
state("Closing CD Drawer", () => {
is("No CD in drawer", config => cdPlayer.cdLoaded === false);
is("CD in drawer", config => cdPlayer.cdLoaded === true);
});
});
state("CD Loaded", () => {
is("end of CD", config => cdPlayer.cdLoaded === true);
on("Eject", () => {
cdPlayer.stop();
cdPlayer.openDrawer();
});
state("CD Stopped", () => {
on("Play", () => cdPlayer.play());
});
state("CD Playing", () => {
on("Pause", () => cdPlayer.pause());
on("Stop", () => cdPlayer.stop());
});
state("CD Paused", () => {
on("Pause", () => cdPlayer.play());
on("Play", () => cdPlayer.play());
});
});
});
This is just a rough pseudocode so let me know what you think....
@camwest Thanks for this! I'm going to make guard conditions, onEntry
onExit
and onTransition
functions able to take in a string as well, because you're right, that's an important use-case (especially for RPC applications).
So, with string identifiers, you would get back a state that looks like this:
const config = {
initial: 'Start',
states: {
CDDrawer: {
initial: 'Closed',
states: {
Closed: { on: { EJECT: 'Opening' } },
Opening: {
on: { OPEN_DRAWER: 'Open' } },
onEnter: 'openDrawer'
},
Open: {}
}
}
}
}
const cdMachine = Machine(config);
const nextState = cdMachine.transition('CDDrawer.Closed', 'EJECT');
// State {
// value: {
// CDDrawer: 'Opening'
// },
// effects: [
// 'openDrawer'
// ]
// }
Then, you can execute the side effects with your implementation:
const nextState = ... // same as above
// dispatch events using Redux (assume dispatch is available)
const reduxEffects = {
openDrawer: () => dispatch({ type: 'OPEN_DRAWER' }),
closeDrawer: () => dispatch({ type: 'CLOSE_DRAWER' })
};
// Execute effects in the order specified by the returned state instance
if (nextState.effects.length) {
nextState.effects.forEach(effectName => {
const effect = reduxEffects[effectName];
if (effect) effect(); // execute effect
}
}
Why are the transitions targeted at states? Shouldn’t the “action” be global in a state chart?
They're not targeted. They're global to the entire state chart. The actions specified on states just inform how the state should transition when:
Also @camwest I'd like to avoid function syntax because functions are not (easily) serializable, and it makes it more difficult to statically analyze a state machine.
@davidkpiano totally I really don't like the function syntax either =)
I'm confused about guards. Shouldn't that logic exist outside of the statechart? For instance, trigger a specific event only given a condition.
If we implement guards this way (inside the statechart) then why not have the logic for "triggering events" also in the statechart? I'm not sure where to draw the line.
Shouldn't that logic exist outside of the statechart?
Yes, so I'll make it so that guards can be constant strings as well, just like onEntry
and onExit
.
But "trigger a specific event only given a condition" is actually an important part of statecharts, if you view "actions with guards" as separate, distinct types of actions. For example, let's say you had a FSM that handles the filling of the cup with states filling
and full
, and external state volume: number
where volume
is from 0 (empty) to 100 (full). You can have transitions from empty/filling
to:
filling
on FILL
if volume < 100
full
on FILL
if volume >= 100
That looks like ad-hoc logic, but you're actually creating distinct subsets of the external state volume
paired with the FILL
action, so that you can think about it as having FILL_NOT_FULL
and FILL_TO_FULL
actions, respectively.
I do think the implementation of guards/events should be outside the statechart though, yes.
Wouldn't you simply trigger a FULL event when volume is >= 100 outside the statechart?
I'm really not convinced about guards at the statechart level (at the actual visual level I mean). Even the original Harel paper doesn't seem to mentioned them, right? But again, I'm kinda new to this space so I may be over-simplifying things.
EDIT actually my bad, they are part of the paper:
We thus enrich the transition labelling to be of the form a(P)/S, where a is the event triggering the transition, P the condition that guards the transition from being taken when it is false, and S the action (or output in automata-theoretic terms) to be carried out upon transition. (We can actually allow Boolean combinations in each component, but we shall not get into a detailed syntax here.)
Wouldn't you simply trigger a FULL event when volume is >= 100 outside the statechart?
No. Actions should never be aware of extended state - they should simply be a stimulus executed in response to an event.
actually my bad, they are part of the paper:
Yep, which is why I included them :)
Gah, sorry I'm confused. How is FILL an action? I don't think actions are capable of triggering transitions... I mean they do trigger transitions in other orthogonal states. So how do you define events and actions in xstate?
Events are any stimulus that occurs in a program.
Actions are the distinct "categorization" of those events to be accepted by the machine.
You map events to actions yourself as a developer. As a trivial example (in React):
<button onClick={() => this.dispatch('CLICK')}>
Click me!
</button>
Above we're mapping the click
events of the above button to the distinct CLICK
action, which is an action that our machine (presumably) understands.
Interesting, however I'm not sure that's the standard formalism defined in Harel's paper. I'll open a separate issue to help us grasp the differences.
Guards are now in v3! http://davidkpiano.github.io/xstate/docs/#/guides/guards
Hey @davidkpiano
Just watched your amazing talk at React Rally.
We currently use https://github.com/winzou/state-machine as a state machine in a php application, the package has a concept of guards, so if I try and call a transition it will first check to see if it passes the guard, if so continue and if not then stop execution. Is this something you could see being implemented in this package?