Facts about State Machines
github.com
github.com
The most common way to abstractly describe state machines is the directed graph, but (like nearly all graph problems) it's often just as illuminating if not more to decompose the graph into a matrix as well. So rows are states, columns are events, and the intersections are transitions. Being able to run down a column and see what happens in each of the states when a certain event is processed has made a lot of bugs much more obvious for me.
Now when documentating a state machine enough to actually draw out the graph, I always include a matrix representation as well for that reason.
It doesn't sound too hard but I'm sure there are wrinkles.
Foreach node
Foreach node.transition
Matrix(node, transition.event) = transition.destination
And the other way: Foreach node
Foreach event
Destination = matrix(node, event)
Graph(node).add(event, destination)The best result was the time we spent an hour at the whiteboard, then the team split up and we each re–wrote part of the code (there was message–passing between components involved, so we just each took one component; I had cleverly used the events between components in the state machine in addition to external events). We all committed by the end of the day, and the next day QA said it was perfect. (Eventually I think some automated tests got written, but whatever.) Compare that to the prior months where QA would find some weird problem, someone would debug it, write a patch, and declare it fixed without realizing that they had broken some other edge case. There were never any bugs found in that thing ever again.
Incidence matrices[1] might be an effective way to represent graphs, but the sparse nature of state machines combined with large numbers of input events and states end up making it hard to represent non-trivial state machines.
Also, state machines are often organized into sub-state machines, which are not adequately represented with incidence matrices.
1. "How I Learned to Stop Worrying and Love the State Machine"
link: http://raganwald.com/2018/02/23/forde.html comments: https://news.ycombinator.com/item?id=16468280
2. "More State Machine Love: From Reflection to Statecharts"
link: http://raganwald.com/2018/03/03/reflections.html comments: https://news.ycombinator.com/item?id=16606379
Author says that SMs and "finite automata" are different things, and links to Wikipedia to prove it. But Wikipedia says "A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation."
https://en.wikipedia.org/wiki/Finite-state_machine
Author seems to take the view that something like a compiler is a "state machine", or at least can be modeled as one. I suspect that his definition (which he doesn't actually set out) translates roughly to "machine".
The most obvious difference is that a state machine can have infinite transitions and infinite states. The most obvious example is a counter machine that simply keep tracks of how many events it received.
Such state machines can be Turing complete, but also allow to add restrictions to limit their expressiveness
https://en.m.wikipedia.org/wiki/Pushdown_automaton
Edit: to add more context Turing machines are essentially finite state machines with an unlimited memory attached and specific input events (the alphabet of the memory eg bits) and output states (move left/right, write, halt; with possibly multiple state for each output)
This is the basis of OO's State pattern.
https://github.com/statelyai/xstate#readme
Interactive visualizer: https://stately.ai/viz
Independently of XState's visualizer and more recent IDE/plugin offerings, XState allows you to declaratively specify statecharts and fsms (vanilla JS object notation), define handlers (functions) for transitions and services, and then instantiate those machines via an interpreter provided by XState. It might not be fun to do all that, say, in a Node.js REPL, but you could.
I'm not aware of sub-state-machines as a standard concept. There are State Charts which are a superset of state machines and have their own notions of composition (XState is an implementation).
A sub-state-machine may feel like an intuitive concept but it does lead to a number of design decisions and related tradeoffs.
For example:
- We can have "State Machine A" influences "State Machine B". In order to make that happen there needs to be code in A that notifies B. Perhaps we do this with an explicit function call B. That's not too bad, but we are introducing a tight coupling between them.
- We can also have "State Machine A" influences a number of downstream state machines. Now we need many function calls or instead implement some observable or event protocol. That's also not too bad but its certainly not out of the box.
- Or we can have "State Machine A" and "State Machine B" both influence "State Machine C". In order to avoid updating C twice (glitches) we need some coordinating code that understand the dependency graph. I know from experience that is definitely not easy or obvious.
- And additionally we can address what it means for state machines to come and go over time which changes the dependency graph.
Behavior Graph is our implementation of those ideas in a slightly more general sense (we aren't restricted to _finite_ state machines and we have a generalized notion of events and observers).
I know talking about things this abstractly can make people's eye's glaze over so I won't belabor it too much, but that's what I mean by "don't compose easily out of the box".
https://en.wikipedia.org/wiki/SCXML
Work on that goes back to the early 2000s, when XML was on the rise. One of the more important and lasting contributions of that work is likely Appendix D, "Algorithm for SCXML Interpretation":
https://www.w3.org/TR/scxml/#AlgorithmforSCXMLInterpretation
I suggest ignoring the XML-specific aspects of it; consider that such tree-shaped data structures can as readily be encoded in JS objects (including stringified JSON). Also consider the larger spec's notions of `send` and `invoke` for "External communications":
Hierarchical State Machine.
Bad Wiki entry (because ATMs are really bad at parsing natural language), but at least it contains a reference: https://en.wikipedia.org/wiki/Augmented_transition_network
Sub-state machines or hierarchical state machines have been a thing in games for quite a long time. It’s a natural extension particularly for AI although IMO it’s mostly reducing a cognitive burden.
That just means you've never looked into it nor have much experience with state machines.
Sub-state machines are even supported as a basic features in some popular GUI frameworks such as Qt.
Qt in turn uses W3C's SCXML format[1] to represent state machines, which not only supports substates but also parallel states.
[1] https://www.w3.org/TR/scxml/
If anything, anyone with any experience writing any parser is already aware that the lexer represents a sub-state machine, and this is close to CS101.
An excellent starting point for this post. I couldn't agree more.
We should start respecting these standards again just not treat them religiously like we did in the 2000s.
Some time ago I wrote up a particularly simple but elegant example I came across. The write-up is here:
https://www.solipsys.co.uk/new/StateMachineInRealLife.html?v...
In case people would like to discuss that specifically, I've (re-)submitted it here:
"A Computer is a state machine. Threads are for people who can't program state machines." - Alan Cox
typedef struct State State, *StateMachine;
typedef void (*Event)(StateMachine *sm);
struct State {
Event TurnOn;
Event TurnOff;
Event MakeStuck;
};
void IdleTransition(StateMachine *sm);
void BecomeOn(StateMachine *sm);
void BecomeOff(StateMachine *sm);
void BecomeStuck(StateMachine *sm);
const State StateOn = {
.TurnOn = IdleTransition,
.TurnOff = BecomeOff,
.MakeStuck = BecomeStuck
};
const State StateOff = {
.TurnOn = BecomeOn,
.TurnOff = IdleTransition,
.MakeStuck = BecomeStuck
};
const State StateStuck = {
.TurnOn = IdleTransition,
.TurnOff = IdleTransition,
.MakeStuck = IdleTransition
};
void IdleTransition(StateMachine *sm) { }
void BecomeOn(StateMachine *sm) { *sm = &StateOn; }
void BecomeOff(StateMachine *sm) { *sm = &StateOff; }
void BecomeStuck(StateMachine *sm) { *sm = &StateStuck; }
StateMachine sm = &StateOff;
It's still C but arguably much more concise than a switch, wouldn't you agree?This is but stupidly primitive representation / implementation of SM (yes it is useful in very simple cases). You can do way better in C. Somebody else already presented more advanced design.
You know, that kind of code:
if(start) {
if(!running) ...
}
if(stopped & !jumping) {
} else if(running) ...
Stop and write a proper case statement (aka a state machine). You may discover that combinations of booleans (in my example, stopped & !jumping) deserve to be their own state. The insight is that the resulting code should be one clean case statement, with no nested ifs allowed. Then you know you cover all the states/cases.That switch-case alternative you’re suggesting? That’s just another manifestation of the same concept behind “If” conditions. So, really, with case statements, you’re still implementing nested conditions, just in mixed ways (a mix of case statements and “If” conditions)
The distinguishing characteristic between state machine and bad logic is that the top level conditions are the states, rather than the inputs.
EDIT: for clarity.
[1] https://dev.to/davidkpiano/you-don-t-need-a-library-for-stat...
Me: So, each instance should represent a state, and include a list of states it could transition to, and methods to transition to them? Is that what you had in mind?
Them: Well, if that's what you think a state machine is, then sure.
Me: There are a million ways I could implement this. What context will this be used in? I can make sure I write something that meets those requirements.
Them: Do you not know what a state machine is?
Me: Of course. But how are you planning to use it?
Them: I can't believe you don't know what a state machine is.
So as far as I can tell, I have no idea how to implement one.
(I'm so glad I didn't get that job.)
def next_state(state):
if state == 90000:
return 0
return state + 1
But more seriously; once I was asked to implement offsetof in an interview for an entry level C++ position. I didn't quite remember that this was impossible during the heat of the interview, but I did remember enough to not produce an incorrect solution that relies on undefined behavior. I didn't get that job.They're probably glad they didn't hire you either.
A better answer would be either "Yep sounds good to me" or something along the lines of "Let's step back and first define what a state machine should do".
Even if GP was not getting the job it is the responsibility of a good interviewer that GP leaves the room having learned something. If GP left this interview still not knowing what a state machine is (or what the interviewer thinks a state machine is) or no closer to an implementation of a state machine then the interviewer has failed as much as the candidate.
I strongly disagree. An interview is an assessment, not a tutorial.
When the CTO says “we need a thing”, I don’t sit down and start writing. I try to learn what we’ll need so I can build something that satisfies my stakeholders’ needs.
If I were interviewing you, and said “hey userbinator, make a paragraph”, and you did that without asking WTF I was talking about, the rest of the session may not go well.
...and chances are that the interviewer would be fine with any of them, or ask questions after you've shown a solution.
If I'd asked such a question, and got no sense of pushback, I'd be a little nervous about hiring that person and having them need close supervision - because they were to scared to point out ambiguity in direction.
That's the exact opposite impression I have; someone who doesn't need to ask many questions, as long as they can justify their decisions well, is definitely more preferable to someone who constantly asks and needs to be lead every little step of the way.
questions like that don't show you how they'll be on the job, they're an opportunity to see their thought process, but you won't get that without talking to them.
Asking for clarification before starting isn't anywhere close to "needs to be lead every little step". If you are hiring a work crew for roadside ditch digging than your interview method might be a good idea, but for a job where implementation details matter - bad idea. One of my more difficult professional experiences was fighting the urge to displace blame for a wasted week of work, which would have been avoided had I asked for the tasking to be described back to me - or had he spoken up and said "Do you mean this or that?"
they may be glad they didn't hire them, it's hard to say how they feel, but i bet they're one of those companies out their complaining they can't seem to hire anyone at any price.
At that point, it's probably best to stop arguing, and code. They probably want to argue with you about your code, not their spec.
But when I got the reply "Do you not know what a state machine is?", I think I might have thanked them for their time, and left. I would have been sorely tempted to come back with "Yes, a state machine is an abstraction, and you seem to want a concrete implementation, but you don't seem to want to fill in the details of your requirement. Is it a good use of our time together for me to complete your spec, so there's something for me to code up?"
It sounds like they didn't want you; perhaps your face didn't fit. They say most interviews are decided in the first couple of minutes.
For example:
If a pull request goes from Draft->In Review the FSM might perform an "assign reviewer" action.
If a PR goes from Changes Requested->In Review, there is already a reviewer assigned, so it just performs "notify reviewer".
Or, perhaps an additional transitional state.
Draft->Closed/Abandoned
Draft->In Review
Changes Requested->In Review
I'm not sure "assign reviewer" would fit either in Draft.exit() or InReview.entry(). I guess you'd implement something like "ensure reviewer assigned" in InReview.entry() instead. Which would work, but would itself contain the sort of state-dependent logic that a state machine is meant to surface.
Depending on the system and implementation constraints one type works better than the other (e.g. Mealy machines can implement Moore machine behavior with one less state, which may save hardware).
A long series of ifs/match/dictonary/whatever to match (current state + input → next state).
That's it.
On the other hand, regular expressions and things like lexers are largely based on finite-state machines so there is a ton of information related to those, but they often implement things more complex than finite-state machines in order to be more expressive.
In terms of manually implementing them yourself, I think the naive implementation based on the mathematical definition of a deterministic finite automata is pretty good for a small number of states for things like tracking program state. In particular, explicitly listing the total set of expected inputs/transition criteria, explicitly listing the states, and having a single function that takes the current state and current transition-relevant data and produces a new state.
This representation is nice because it makes the behavior inspectable in one location. This makes it easier to notice the edge cases and prevent the state representation and possible transitions from getting spread all over code. The transition function can be a switch statement or a table. Really any way of writing a two-input function with a finite number of inputs and outputs will work. Many people have also explored strongly-typed versions of this, which are worth a look as well.
enum State { S1, S2, S3, S4, ... , END }
State transition(State current_state, int input) {
switch current_state
case S1:
switch input
case 100: new_state = S3
do_action1()
case 101: new_state = S4
do_action2()
default: error()
case S2:
switch input
case 100: new_state = S3
do_action3()
case 206: new_state = S11
do_action4()
case 207: new_state = S12
do_action5()
case 208: new_state = S19
do_action6()
default: error()
case S3:
switch input
...
...
return new_state
}
run_state_machine(int[] inputs) {
State state = S1
foreach input in inputs
state = transition(state, input)
}
That's it. It's simple to understand and it's clear what the legal transitions are from each state based on the input. You can attach actions and data update on each state transition to do something useful. enum State { A, B, C };
enum Action { A, B };
type StateData =
| { state: State.A, data: whatever }
| { state: State.B, data: whatever }
| { state: State.C, data: whatever };
type ActionData =
| { type: Action.A, data: whatever }
| { type: Action.B, data: whatever };
function transition(prev: StateData, action: ActionData): StateData {
switch(action.type) {
case Action.A:
return { type: State.B, data: whatever };
case Action.B:
return { type: State.C, data: whatever };
}
}
Then you can for example if you were using this in react do something like function MyComponent() {
const [state, setState] = useState<StateData>({ state: State.A, data: whatever });
return <button onClick={() => setState(transition(state, { action: Action.A, data: whatever }))}>Go!</button>;
}Basically, you have a variable that holds an (immutable) object representing the current state. All states that can be assigned to that variable (all abstract states the system can be in) implement a common interface (the static type of the variable), which in particular includes a method (or methods) for asking the state what the next state is given a particular event. The method then returns the new state. Whenever an event occurs, you ask the current state stored in the variable (call its respective method) to compute the next state given the current event, and then you assign that new state returned by the method to your state variable.
The common interface usually comprises further methods that define/implement the behavior of the system in the given state. For example, if the states correspond to the different modes of an editor, there could be a method that implements what happens when a key is pressed in the specific state.
state_x:
... do some stuff ...
if(some_condition)
goto state_y;
else if(some_other_condition)
goto state_z;
else
goto state_n;
...
state_y:
... # (state, event) => (next state, action)
FSM = {}
Add transitions (using enums): # Simple left down-[move*]-up sequences.
FSM[STATE.clear, EVENT.left_down ] = STATE.set_caret, 'set_insertion_point'
FSM[STATE.set_caret, EVENT.left_motion] = STATE.set_caret, 'set_insertion_point'
FSM[STATE.set_caret, EVENT.left_up ] = STATE.clear, 'nothing'
Then when an event happens you drive the FSM like so: new_state, action = FSM[state, event]
Check out https://git.sr.ht/~sforman/Xerblin/tree/trunk/item/xerblin/g... for an example.There is also a DOT file (and SVG image) of the resulting state graph (the code to generate the DOT file directly from the FSM dict is at the bottom of the mousebindings.py file.)
state = state.next(context, data);
Context just holds the global stuff of the process, data is the new input into the machine and state is the current state.A simple example is an SMTP server. State is the current command (or idle), context is email message being built, and data is the line read from the socket.
Context context = new Context();
sendSMTPBanner();
State state = new IdleState();
while (!state.isFinished) {
String data = readLine();
state = state.next(context, data);
}
Conceptually pretty simple.https://cs.brown.edu/~sk/Publications/Talks/SwineBeforePerl/
https://cs.brown.edu/~sk/Publications/Papers/Published/sk-au...
> Combinatorial explosion can be mitigated through nesting and parallelizing
You can also add an intermediary state to handle this. Instead of NxN you can have Nx2 by doing N->I->N.
I've used this library very succesfully in such a situation: https://github.com/kenkunz/svelte-fsm
Svelte + FSM turned one of the most convoluted UXes I've every dealt with into something manageable and almost pleasant.
This is a complex sales kiosk that must operate work limited or flapping network connectivity entire handling huge spikes in usage.
Svelte itself jusy gets out of the way, you don't fight it. The FSM makes your mental model explicit.
Something like "ok, we're halfway through the complex terminal registration process, the last network request timed out and the user pressed the back button, we have half the necessary data cached in indexeddb, now what?"
With boolean flags its a mess.
Totally undeused concept I've been trying to implement since the VB6 days.
https://www.wisdom.weizmann.ac.il/~harel/SCANNED.PAPERS/Stat...
https://web.stanford.edu/class/archive/cs/cs103/cs103.1142/b...
Done correctly, you can disentangle the decision-making code from the side effecting code to maximize reasoning ability.
My favorite example of this is the following write up: https://gist.github.com/andymatuschak/d5f0a8730ad601bcccae97...
Now, you could argue that this design is not a state machine but I think its more useful to understand that such a thing that is VERY similar to a state machine but has non-atomic transitions does exist and is commonly used.
The bottom line is: States are defined by how the system reacts to events. Insofar as the system always reacts to events in some way, it is always in a specific state.
Anyway, my point was that these are all just conceptual models and another conceptual model that's useful is one with non-atomic transitions.
For example, consider a state A of some object in your game. For event E (say, the player presses some specific button), the object may transition to state B. For a different event F (say, the object collides with a particular type of other object), it may transition to state C. For yet another event G, the object may remain in state A. Thus the function corresponding to A is { E -> B, F -> C, G -> A } (assuming that E, F, G are the only possible events).
Now, you’re saying that the object can be within the transition from (say) A to B. The question is, if one of the possible events occurs during that transition, what does the corresponding function look like? For example, if event F occurs, does the object still continue to go to B, or does it maybe now go to C, or to yet another state? You can write down the corresponding function. If that function is the same as the A function, then that means that the object is actually still in state A. If the function is the B function, then it means the object is actually already in state B. If the function is different from both the A and the B function, then it means the object is actually in a third separate state.
Note that an event can also be “the transition animation has finished playing”.
The point is, using that functional definition of states (which is the usual definition of state machines or FSAs), for any system with non-atomic transitions there is an equivalent state machine with atomic transitions, and that latter one is what is normally understood to constitute a proper state machine.
Tools that would integrate well with any code in JS. Maybe you would define blocks with code, and wire the things up in the IDE but with a graphical interface. Kind of like how draw.io is integrated in Jetbrains IDE (and certainly in VSCode too).
There is a basic UI available for editing the tree https://github.com/BehaviorTree/Groot