Initial import of "Facts about State Machines" document.

This commit is contained in:
Chris Pressey 2022-09-24 11:11:41 +01:00
commit 9f5e65faf6

533
README.md Normal file
View file

@ -0,0 +1,533 @@
Facts about State Machines
==========================
Jump to: [Introduction](#introduction) | [Fundamentals](#fundamentals) | [Intermediate](#intermediate)
| [Statecharts](#statecharts) | [Implementation](#implementation) | [Advanced](#advanced)
## *Introduction*
### I hold the opinion that state machines are often misunderstood and under-applied
And that's why I wrote this. The goal of this list of facts is not to teach
you what state machines are or how to use them; there are plenty of other resources for that.
Rather, the goal here is to motivate their usage and to highlight things about
them that are frequently overlooked, but nonetheless relevant.
If you are reading one of these facts and deem it trivial, or irrelevant, or
unsubstantiated, or, or, ... then I encourage you to just move on to the next one
in the hopes that you will find it more edifying.
## *Fundamentals*
### State machines are ubiquitous
They're all over the place. Think of all the web apps, all the electronic devices,
all the household appliances, all the industrial machines that people interact with
every day. For each one of them, we can formulate a state machine that describes
its reactive behaviour with a great degree of precision.
### A state machine is not a state machine diagram
A state machine is an abstract object -- a concept. A state machine diagram
is a depiction of that abstract object. Much like how the same number can be
equally well represented by a numeral (like "5") or a word (like "five"), the
same state machine can have multiple representations, different but equivalent.
### A state machine diagram is like a *map* of a system's reactive behaviour
It is like a map because, for a given state, you can see what transitions are
allowed into and out of that state. You can see the paths by which the system
"travels" from one state to another, as well as seeing where there are no paths.
Just like a map, it doesn't have to (and probably shouldn't) contain every single
detail. It needs only enough detail to allow you to orient yourself efficiently.
### A state machine is like a board game, and the diagram is like the board
You can imagine you have a counter, and it's sitting on one of the squares
(states) on the board. You draw a card, and written on it is a word (an input).
Now, if there's an arrow going out from your square
that is labelled with that word, you slide your counter along it, moving
it to a new square. But if there's no such arrow going out of your square,
your counter stays where it is.
### State machine diagrams can be read by non-technical stakeholders
Maps and board games are everyday things, so I hope the previous two metaphors
convince you that just about anyone, technically-minded or not, can grasp the
basic functioning of a state machine.
A non-technical stakeholder might still find a large diagram intimidating,
of course. But by splitting a diagram into smaller diagrams,
or by reducing a diagram to a simpler diagram by leaving out details,
you can ease the reader into the structure of the reactive behaviour of the system.
### A state machine diagram allows reasoning about the system's reactive behaviour
If you are given a starting state and series of inputs, you can plot a
course through the state machine diagram and see what state you end up
in, and what actions occurred along the way. By comparing this with your
intuition, or with reports of how the actual system actually behaves
(or specifications for how it _should_ actually behave), you can pinpoint where
changes need to be made.
### A state machine diagram allows reasoning about proposed modifications
Suppose we want to introduce a new operation into our system. If we did,
what would happen in existing states when the events associated with that new
operation occur? What _should_ happen? Will new states need to be added
to capture what is going on with these events? And how will those states
relate to existing states?
### State machines can be used for specification
A state machine diagram, presented prescriptively, establishes
the reactive behaviour that your system _should_ have. The code itself can only
describe the behaviour that your system _does_ have. The only way to
decide if the exhibited behaviour is correct, is to compare it to the
behaviour it _should_ have. This is the idea of having a specification.
### State machines can be used to model user interfaces
Having heard somewhat uninformed statements like "I thought state machines
were only for physical devices" or "User interfaces aren't suitable for
modelling with state machines", which are plainly false, I feel I should
point this out.
In fact, there is an entire book about designing UIs using Statecharts,
[Constructing the User Interface with Statecharts (Horrocks, 1999)](https://archive.org/details/isbn_9780201342789)
(archive.org, borrowable online.)
It is noted in the book that using the Statecharts notation
([see below](#statecharts)) rather than a
flat state diagram will be helpful (virtually necessary) to address the
complexity found in most user interfaces.
### Most state machines are simple
A compiler, for example, has trivial reactive behaviour. Diagramming
its state machine might not be the best use of your time.
But even simple state machines are sometimes worth working out as
diagrams, simply to confirm that they _are_ simple, and in exactly what way.
### A complex state machine means your system has complex behaviour
And anything interfacing with this system -- such as a human using it -- will face this
complexity, and the cost of this complexity. This may prompt you to review the
behaviour. Maybe the complexity is inherent... or maybe it can be simplified.
### Working out a state machine helps you understand your system's reactive behaviour
You won't get it right on the first try, and probably not the second or third try
either. But each time you revise it, you'll learn something important about how
your system behaves.
### Studying a state machine can help you simplify the design of the system
To reduce the complexity of the system, you should eliminate complex things, like
concurrency and complex state interactions, where-ever you can. Going through the state
machine diagram will help you understand where complex state interactions occur,
and may help you work out where they are actually needed, and where they could be
eliminated.
### Understanding a state machine will help you formulate useful tests
An arrow shows a transition that can happen when an input occurs. What states can it
happen from? We can look at the state machine diagram and answer that. Now,
what about those _other_ states -- what should happen when the input occurs in those states?
What _does_ happen? Test cases can be formulated to cover these possibilities.
## *Intermediate*
### State machines and finite automata are two different things
While similar in structure, the two are quite different in practice.
Finite automata (sometimes also called "finite state machines") are formally
defined abstractions in [automata theory](https://en.wikipedia.org/wiki/Automata_theory) (Wikipedia).
They are able to recognize some formal languages and not able to recognize
others, and they have practical applications in software, for example in
pattern matching in text documents.
A state machine, on the other hand, is a system design concept that originated
in [electrical engineering](https://en.wikipedia.org/wiki/Electrical_engineering) (Wikipedia.) It relates the
states of a system by the possible transitions between them; those transitions are
triggered by inputs received from external sources. The inputs are received at
(nominally) different times. Making a transition, or arriving in or leaving a state,
may be guarded by a condition, and may trigger an action of some kind as a side-effect.
There is no pre-set constraint on the kinds of actions such a transition may trigger.
### The phrase "finite state" can be misleading and can be avoided
While a state machine typically does indeed have a finite and fixed number of _states_,
there is no set limit on the amount of _state_ that may be associated with the machine.
That state typically includes counters, timers, buffers, data stores, and so forth.
It might help to note that the word "states" is what's called in English a _count noun_
(like "pennies"), while the word "state" is a _mass noun_ (like "water").
Some terminology that has been proposed to make this distinction clearer includes
calling the finite set of states the "control state" and everything else the
"extended state". Considering both of these together is sometimes called
"the state of the system".
The term "state machine" is now commonplace; you don't have to qualify it with
"finite". System designers will usually understand what you mean when you say
"state machine".
### The "states" of the machine refer to the _modes_ of the system
There's a good argument to be made that the word "mode" would be a better
choice than the word "state" for referring to a member of the finite set of
control states.
If you ask yourself, "What modes can this system be in?", you'll likely
find that the answer coincides with the set of states of the state machine.
It's also consistent with the fact that this set is finite: it's
difficult to imagine a system with an infinite number of "modes".
But a terminology shift of this magnitude is unlikely to happen anytime
soon. Everyone knows them as "state machines", and calling them
"mode machines" just wouldn't catch on.
### "States" are often qualitative while "state" is often quantitative
It's also been noted that the finite set of states tends to describe the
_qualitative_ state of the system, while the "extended" state tends to be
_quantitative_ in nature (counters and such).
### Being in exactly one state at any given time is helpful for reasoning
This quality of _mutual exclusion_ is very helpful when reasoning about
the behaviour of a state machine because, if the machine is in (for
example) the "waiting" state, you immediately also know that it is
_not_ in the "ready" state, and _not_ in the "rejected" state, and...
and so forth, for all other states.
### ...but the set of states need not be disjoint
Although it is very, very useful for reasoning about reactive behaviour
(see previous fact), it's not gospel. The Statecharts paper (Harel, 1986)
([see below](#statecharts)) gives an
example of a diagram that shows two partially overlapping states
in Figure 41.
### A state machine diagram is not a flowchart
A flowchart is "self-propelled"; the transition that is followed out of a
decision point is determined solely by the decision made inside the decision point.
In a state machine, though, the triggers that cause the transitions are generally
_external_ to the system. (It's _re_ active, remember.)
### ...but you can incorporate some flowchart elements in a state diagram
Sometimes it happens that an input causes a transition, but the ultimate
destination of that transition is not solely determined by the input; one
or more decisions have to be made in order to select the state to which
the machine will ultimately transition. This can be captured with
multiple arrows between the same two states, with different conditions on
each arrow; or with "empty" states that don't represent states so much as
they represent decision points. But there are valid objections
to both of these approaches -- the conditions can be complex, and the
"empty" states are artificial. So there really is a case to be made that
decision points, like the diamond boxes from a flowchart, can be used in
a state machine diagram, when using them would make the behaviour clearer.
This should be recognized as being an advanced technique, however.
### A state machine does not specify the means by which the inputs are communicated
That is to say, there is no requirement that the inputs of the
state machine are messages sent directly to the state machine.
The state machine could be "listening for" and reacting to broadcast
events that other systems are reacting to as well.
Also, if you have two state machines sending messages to each other,
the question of whether those messages reside on a queue while in transit,
or any other detail of the implementation of their conveyance, is really
not in the scope of the state machine design itself.
### Inputs are always defined at some level of abstraction
For example, _mouse button depressed_ and _menu item selected_ are both events,
but the latter is at a higher level of abstraction than the former; indeed,
pressing the mouse button down may be one way of selecting a menu item
(while there might also be other ways, such as using keyboard navigation).
A state machine can exist at any level of abstraction, but typically exists
only at that one level; events from other levels usually need not be considered
in its design.
### State transitions are atomic
There's a simple proof by _reductio ad absurdum_ for this one: if state transitions
_weren't_ atomic, then you could introduce another state in the state diagram
that describes the intermediate state -- the state of the system while the transition
is taking place. And, naturally, you would have transitions into and out of that
intermediate state. But then, what would be stopping you from introducing
further intermediate states for _those_ transitions, and for the transitions into
those further intermediate states... and so on unendingly? It would be an infinite
regress. Therefore -- at the level of abstraction that the state machine is dealing
with -- transitions between states must be atomic.
### State machines can also describe lifecycles of objects
Traditionally, the subject of a state machine diagram is some kind of interactive
system, but many other kinds of objects undergo state transitions over time as well.
These tend to be on longer timescales. For instance, a loan application
might be in one of the following states at any given time: being filled out,
submitted and waiting for evaluation, in evaluation, accepted, or rejected.
### Lifecycles tend towards linear, interactive systems tend towards cyclic
This should be apparent from the word "life", which implies birth and
death.
Often, state machine diagrams have distinguished notation for
"start state" and "end state". These apply more strongly
to a lifecycle of an object than to an interactive system, where
the concept can be somewhat subjective. (Is the start state of
a cardboard box the open state or the closed state?)
## *Statecharts*
### State machines have the potential for combinatorial explosion
Say you have an array of four on/off switches. Each switch
can be in 2 possible states. But the array considered as a whole
can be in 2 times 2 times 2 times 2 (= 16) possible states, and
each of these states transitions to 4 other states.
A state diagram that includes all these states and transitions
would be unwieldy -- more cumbersome than helpful.
### Combinatorial explosion can be mitigated through nesting and parallelizing
In the array example above, it's unlikely that a significant number
of those 16 states have any meaning, in the sense of influencing
what the next state should be based on a given input. It's likely
that the states are, for the most part, treated homogeneously. So it
behooves us to have a notation that captures this, instead of naively
treating all states equally.
In the Statecharts notation, for example, the array could be depicted with
each switch being its own 2-state state machine nested inside an orthogonal
region (that is, a parallel partition) of a single superstate representing the
entire array.
### Statecharts was introduced by David Harel in the 1980's
The original paper introducing Statecharts can be found here:
[Statecharts: A Visual Formalism for Complex Systems (Harel, 1987)](https://www.wisdom.weizmann.ac.il/~dharel/SCANNED.PAPERS/Statecharts.pdf) (PDF).
Statecharts has been a very influential notation for state machine design,
but continues to be often misunderstood and under-appreciated
(much like state machines themselves.)
Major contributions from it are _hierarchically nested states_ and
_orthogonal regions_ within a superstate.
### Statecharts was adopted into OMT
OMT (Object Modelling Technique) was a software engineering methodology
formulated in the late 1980's. OMT promoted a 3-axis design, consisting of
structural, functional, and dynamic aspects. For the notation for the dynamic
aspect, OMT adopted Statecharts. They were originally going to use their
own tree-based notation for nested states, but decided that Statechart's
contour-based notation was a better choice. Details can be found in bibliographic
notes of Section 5 of the book
[Object-oriented Modeling and Design (Rumbaugh et al, 1991)](https://archive.org/details/objectorientedmo00rumb)
(archive.org, borrowable online.))
### Statecharts went on into UML
UML (Unified Modelling Language) is a set of software engineering practices and notations
that grew out of the OMT and Booch formalisms. One of the concepts that UML
provides is the [UML state machine](https://en.wikipedia.org/wiki/UML_state_machine) (Wikipedia),
which has been strongly influenced by Statecharts, supporting the same
major contributions: hierarchically nested states and orthogonal (i.e. parallel) regions.
## *Implementation*
### There are many ways to implement a state machine
In the words of SCTV's fictional Mayor Tommy Shanks, "There are so many ways to
implement a state machine, you could shake a stick at them." The paper
[The Anthology of the Finite State Machine Design Patterns (Adamczyk, 2003(?))](https://www.hillside.net/plop/plop/plop2003/Papers/Adamczyk-State-Machine.pdf) (PDF)
lists dozens of such ways (mostly variations on the State Design Pattern).
Even outside of object-oriented designs, there are a number of choices
that can be made: to use a lookup table, to switch on state for a
given input, to switch on input for a given state, to have dedicated
input code in each state, and so forth.
### A state machine's implementation can closely resemble its design
Notwithstanding the previous fact, it is often possible to pick a
a straightforward implementation method and run with it. With
prudent software design choices, the conceptual distance between the
design elements (states, transitions) and the implementation elements
(states, transitions) will be small.
This makes state machines all the more attractive because implementing
one is almost mechanical; and checking the implementation against the
specification is also made easier.
(The caveat of using the implementation itself *as* the specification
still applies, though: the code itself cannot tell you what the code
*should* do.)
### There are software libraries for implementing state machines
You may or may not benefit from using one. Implementing a state
machine from scratch is not difficult, and it pays to choose
a software design (see the previous 2 facts) that fits your problem
space -- which a generic library might not provide. So using a
library might be less beneficial than you might imagine (certainly
less beneficial than e.g. a library for reading JPEGs -- something
that is difficult to implement and doesn't admit so much variation.)
### The states are often implemented as an enumeration
Lots of programming languages provide something like this; for example,
C has an `enum` type, and Haskell has a disjoint sum type (as part of
algebraic data types). It's a good fit for the finite nature of the
set of control states; it makes for more readable source code, and
(in a type-checked setting) can prevent a state index variable from taking
on illegal value (that is, one that does not represent any state).
### Boolean flag creep introduces impossible states
State machines are sometimes said to be a remedy for architectural decay,
which means, in part, the tendency for a code base to grow a
new boolean flag every time a new condition is identified. Suppose you're
writing a user interface. You start with a basic implementation, then you
realize you need to wait until some backend service is ready. So you add a flag,
`is_ready`. Then you realize some operation might not succeed and you need
to signal the user of this. So you add another flag, `is_error`. Now,
you have the possibility of *both* of these flags being true. But is that
a meaningful situation? Probably it's not. At this point it's best to
rethink the set of states your user interface can actually be in, and
formulate an enumeration that matches those states,
and use that enumeration instead of these individual boolean flags.
### You might find that some of the states are "derived states"
Suppose you are writing a billing system. One of the requirements is
that when a new invoice is created, it has no line items. Another of
the requirements is that it should not be possible to submit an empty
invoice.
So in the state machine, "Empty" and "Non-Empty" are modelled as states
the invoice can be in, and the "submit" input only has an effect in
the "Non-Empty" state, where it transitions to "Submitted". This very
clearly illustrates the requirements.
But when implementing this state machine, it might not make
sense to formulate an enumeration with both "Empty" and "Non-Empty"
in it, because these conditions really depend on the number of lines the
invoice has -- a quantitative piece of state. They're both instances
of a more general state, "Being Edited".
So this is a case where the implementation could diverge from how
the diagram is drawn. Conceptually, "Empty" and "Non-Empty" are
distinct states, for clarity, but in the implementation, they are
implemented as "derived states" instead of parts of the
"enumerated state". Alternately, you could adjust the diagram
to have a single state, and have the "submit" transition guarded
by the condition "line item count > 0".
See also "qualitative vs quantitative state", above.
## *Advanced*
### Logical statements about state machines can be decided by model checking
We can establish a set of propositions, where each proposition is true at some
states and false at the others. We can then give formulas in temporal logic,
such as "there exists at some future a time when p and q will always be true",
and run a model-checking process on our state machine to see in which states
of our state machine the formula holds.
For more information of model checking using temporal logic, see, for example,
chapter 3 of
[Logic in Computer Science (Huth and Ryan, 2000)](https://archive.org/details/logicincomputers0000huth)
(archive.org, borrowable online).
This comes down to the fact that temporal logic is a specialization of _modal logic_;
state machines are close relatives of _labelled transition systems_; and
modal logics are a natural (or could we say supernatural!) fit with labelled
transition systems. For more on this view see, for example,
[First Steps in Modal Logic (Popkorn, 1994)](https://archive.org/details/firststepsinmoda0000popk)
(archive.org, borrowable online).
### Nesting state machines enables a kind of modularity
Suppose, in the spirit of modularity, we want some part of our state
machine to be "pluggable": we want to be able to swap it out for an
alternate implementation of that part, one that behaves differently
in some ways but is still overall compatible with the rest of the state machine.
For this to work, a modularity boundary must be established around this
part. As long as whatever is inside the boundary reacts like everything
outside the boundary expects, those insides can be swapped out for other
insides that meet the same expectations.
If those insides have their own reactive behaviour, which they probably
do, then the best way to model this is to put the modularity boundary
around a single superstate, which has its own substates (basically its
own sub-state machine.)
For this to work, the substates must be "private"; everything outside
the modularity boundary must not rely on knowledge of them, lest
problems pop up when we try to swap out the superstate with one that
has different substates.
### The "History pseudo-state" can be avoided by use of a convention
Statecharts defines a "history state" (capital H in a circle).
Its is more than a little confusing but, from what I can tell, it is
not actually necessary in many situations, especially if one adopts the following
default behavior:
When re-entering a state _s_ that contains substates, the current substate among
those substate is the same one that was current when _s_ was previously left.
If _s_ was never previously left, then one must consult the start state(s) inside
_s_ to discover which the current substate.
So this is sort of a "history by default" approach. _If_ one wished to transition
from some outside state into a specific substate of _s_, one may do so by having
a transition arrow go, not to _s_, but directly to the desired substate.
Transitions directly into substates like this can be very
useful, but do note they violate the "privacy" requirement of the previous
fact, so they work against modularity (which might or might not be a problem,
depending on the situation.)
An alternate approach to account for modularity might be to decorate
arrows transitioning to _s_ with side-effects such as "Reset _s_ to its
start substate" instead of having them transition to a substate inside _s_.
### State machines are neither imperative nor functional
When the machine switches states, did it destructively update the current
state of the system, or did it create a new state of the system and switch to
that new state? You can implement it either way. The state machine itself
doesn't care.
### State machines demonstrate a weakness of toggling
Suppose we have bursts of events -- sometimes you don't just get one event,
you get a bunch of them. Physical contact switches are like this -- the
phenomenon is called "bounce".
Suppose further that we have 2 states, On and Off, and a "toggle" input that
transitions both from On to Off and from Off to On. If it receives a burst
of "toggle" inputs, it is not possible to predict which state it will end
up in.
But suppose instead we have 2 inputs, "turn on" and "turn off", each of
which transitions from only one state to the other. Now, if we get a
burst of "turn on" (resp. "turn off"), we know what state we will always
end up in: On (resp Off).
There are other situations similar to bursts, e.g. simply not being
aware of which state you are currently in, so this informs good design.