Statecharts · Lesson 03

Orthogonality

Regions stop the diagram doubling. The harder skill is telling a real region from two facts that only look independent — and reading a box as a statement about where a feature applies.


Two facts, side by side

A parallel state is divided into regions by dashed lines. When the parallel state is active, every region is active, each in one of its own states. The configuration contains one leaf per region, not one leaf overall.

stateDiagram-v2
  state car {
    state doors {
      [*] --> closed
      closed --> opening : open
      opening --> door_open : opened
      door_open --> closing : close
      closing --> closed : shut
    }
    --
    state motion {
      [*] --> idle
      idle --> up : go_up
      idle --> down : go_down
      up --> idle : arrive
      down --> idle : arrive
    }
  }
  
class Elevator(StateChart):
    class car(State.Parallel):
        class doors(State.Compound):
            closed = State(initial=True)
            opening = State()
            ...
        class motion(State.Compound):
            idle = State(initial=True)
            up = State()
            ...

Four door states and three motion states. The flat equivalent has twelve, and every door arrow has to be drawn three times. That is the arithmetic, and it is the boring half of the argument.

Events are broadcast

The interesting half is what happens to an event. It is not routed. It is offered to the whole chart, and every region independently either has a transition for it or does not. One b can light the watch and start the stopwatch, because two regions both have a b arrow and neither knows about the other.

This is what keeps regions from degenerating into mutual references. Harel counted broadcast as the third construct precisely because orthogonality without it would force every region to name the others.

The test for a real region

Splitting into regions is only sound if the regions are genuinely independent. The test is blunt:

Can every combination of leaves occur? If some pair is impossible, the regions are not orthogonal — and if you make them regions anyway, you have created states the machine believes in and reality does not.

Doors and motion pass, nearly. All twelve combinations are reachable; one of them — moving with the doors open — is merely forbidden. That distinction is the whole design decision:

Impossible — the combination has no meaning. Model it out of existence: one region, or a compound state, not two regions.
Possible but forbidden — the combination means something and must not happen. Keep the regions and write one guard on the transition that would cause it. A product of states would need the constraint restated in every copy.

The elevator's rule is a single condition on a single arrow:

go_up = idle.to(up, cond="doors_are_shut")

def doors_are_shut(self):
    return self.closed.is_active

Harel writes this kind of guard as in(state) — a condition that reads another region's configuration. It is the sanctioned way for regions to constrain each other, and it is one line rather than a multiplied diagram.

Library note. self.car.doors.closed.is_active is always False. Chained attribute access reaches the class attribute, which knows nothing about any instance. Every state is also published on the instance under its own id, and only that accessor is live: self.closed.is_active.

Scope: the box is the specification

Here is the part that does not show up in the arithmetic, and it is Harel's own favourite point. His watch has a beeper test: press two buttons together and a healthy beeper beeps. Where does that feature apply? Not in dead. Not while an alarm is already beeping. On the real watch, it turns out to work in the time display, in the date display, and throughout the update sequence — but not during a two-second wait. He remarks that the manufacturer's manual describes the beeper test and the light in exactly the same way, “no indication of the scope is provided, despite the major difference between the two”.

In a statechart the answer is not documentation. You draw a box around the states where the feature applies and attach the region there. The light's region sits at the top, so the light works everywhere. The beeper test's region sits around three displays, so it works in three displays. The scope of a feature is the box you drew it in — and it is checkable, because a reviewer can see the box.

stateDiagram-v2
  state alive {
    state main {
      state displays {
        state testable {
          state shown {
            [*] --> time
            time --> date : d
          }
          --
          state beeper {
            [*] --> quiet
            quiet --> beeping : b+d
          }
        }
        testable --> stopwatch : a
        stopwatch --> testable : a
      }
    }
    --
    state light {
      [*] --> light_off
      light_off --> light_on : b
    }
  }
  

Press b in the stopwatch: the light comes on, and the beeper test does not fire, because beeper is not in the configuration. Nobody wrote a guard for that.

Exercise 1 — region, or not a region

For each pair of facts: two orthogonal regions, one compound state (because the combinations are impossible), or two regions plus a guard (because the combination is possible but forbidden)?

Exercise 2 — read the scope

Using the watch fragment above.

Exercise 3 — recall

Next

Something was lost when the alarm interrupted the display in lesson 01: the configuration no longer recorded what had been on screen. Regions do not help — the region was exited. What does help is history.

← Lesson 02Status mapLesson 04 →

Sources: Harel 1987 §3–4 · statecharts.dev: parallel state · SCXML · RESOURCES.md