Statecharts · Lesson 04
History
Memory for where you were, written by the engine rather than by you. Two depths, one common mistake about which arrows consult it, and the question nobody asks until the second run: when should it be forgotten?
The thing interruptions destroy
An alarm goes off. The watch leaves whatever display it was showing, beeps for thirty
seconds, and comes back. Comes back where? The configuration cannot say: displays
was exited, and exiting a compound discards which child was active.
You can get it back by hand — store the leaf in a variable on exit, read it on entry — and people do, and it is wrong in a specific way: the variable can hold a state that no longer exists, or one from a different branch, and nothing checks it. Harel gave the notation its own symbol instead.
Shallow and deep
Harel: “an H generally means that history is applied only on the level in which it appears”. Attach an asterisk — H* — and it applies all the way down. The difference only shows up when the remembered child is itself compound.
stateDiagram-v2
state loaded {
state playing {
[*] --> normal
normal --> fast_forward : ff
}
[*] --> playing
playing --> paused : pause
paused --> playing : play
}
loaded --> interrupted : call_starts
interrupted --> loaded : call_ends
Suppose the player is in loaded → playing → fast_forward when
a call comes in.
Shallow H on loaded restores playing,
then enters its default: back at normal. The fast-forward is lost.
Deep H* restores the exact leaf: fast_forward.
Neither is the right answer in general. Shallow is right when re-entering the inner machine from the top is what you want — a wizard step that should restart. Deep is right when the interruption was supposed to be invisible.
class loaded(State.Compound):
...
h = HistoryState() # shallow
h_deep = HistoryState(type="deep") # deep
call_ends = interrupted.to(loaded.h)
call_ends_deep = interrupted.to(loaded.h_deep)
interrupted.to(loaded) enters loaded's default child, no
matter how much history has been recorded. Only an arrow that names the history pseudo-state
consults it. So a chart can perfectly well have some entrances that resume and some that restart
— which is usually what you want, and is why Harel drew it on the arrowhead.
History is not context
Both are memory that survives a transition, so the line matters:
The test: if the thing being remembered has an unbounded or arithmetic range, it is context, and trying to make it states is the mistake. If it is “which of these boxes”, it is history, and trying to make it a variable is the mistake.
Forgetting on purpose
Harel adds one more move, and it is the one people miss. Once the watch chart accounts
for the battery being removed, an H entrance can no longer be read as plainly “enter the
most recently visited” — history “is to be ‘forgotten’ if
dead has been entered in the meantime”. He introduces
clear-history(state) and clear-history(state*) as actions and attaches them
to the transitions into dead. Once forgotten, defaults are used.
The general shape: history is state that outlives the states it describes, so
something has to own the question of when it stops being true. A session ends, a device resets, a
document is closed — each is a place where a clear-history belongs, and forgetting
to put one there produces a bug that only appears on the second run.
python-statemachine has no
clear-history action, but it keeps what each history pseudo-state remembers in a plain
dict on the instance: self.history_values, keyed by the pseudo-state's id.
self.history_values.clear() in the action on the transition into dead is
the whole implementation. P5 tests it.
Exercise 1 — shallow or deep
Using the player chart above.
Exercise 2 — history, context, or neither
Exercise 3 — recall
Do the problem
P2, the media player puts both history depths over the same compound so the difference is observable, and adds a second region to prove that muting cannot move the playhead. Eleven tests.
Sources: Harel 1987 §2.4, §4 · statecharts.dev: history state · python-statemachine: states · RESOURCES.md