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.

History pseudo-state. A target, drawn inside a compound state as an encircled H, meaning “enter the child of this compound that was active when it was last exited”. Pseudo-state because the machine never rests there: it is a marker on an arrow's head, resolved at the instant the transition fires. First visit, with nothing recorded, falls back to the compound's default child.

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)
The mistake everyone makes once. History is a property of the arrow, not of the compound. 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:

History remembers a state, from a fixed finite set, and is written by the engine on exit. You cannot put a wrong value in it. It answers “where was I?”
Context (Harel: extended state) remembers a value — a counter, a queue, a floor number — and is written by your actions. It answers “how much / which one / how many?”

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.

Library note. 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.

← Lesson 03Status mapP2 →Lesson 05 →

Sources: Harel 1987 §2.4, §4 · statecharts.dev: history state · python-statemachine: states · RESOURCES.md