Nested States

The UML2 specification allows for the concept of a nested state.

Nested states exist to reduce exponential 'state' and 'transition' explosions that can occur with classic FSMs.

Let's look at a code example and the associated diagram.

 var machine = StateMachine.create((g) => g
    ..initialState<S>()
    ..state<Alive>((b) => b
      ..onEnter((s, e) => print('onEnter $s as a result of $e'))
      ..onExit((s, e) => print('onExit $s as a result of $e'))
      ..on<OnBirthday, Young>(condition: (s, e) => human.age < 18, sideEffect: () => human.age++)
      ..on<OnBirthday, MiddleAged>(condition: (s, e) => human.age < 50, sideEffect: () => human.age++)
      ..on<OnBirthday, Old>(condition: (s, e) => human.age < 80, sideEffect: () => human.age++)
      ..on<OnDeath, Dead>()
      ..state<Young>((b) => b)
      ..state<MiddleAged>((b) => b)
      ..state<Old>((b) => b))
    ..state<Dead>((b) => b
      ..on<OnGood, Budist>(condition: (s, e) => s == Dead)
      ..on<OnUgly, SalvationArmy>(condition: (s, e) => s == InHell)
      ..on<OnBad, Christian>(condition: (s, e) => s == InHeaven)
      ..state<InHeaven>((b) => b..state<Budist>((b) => b))
      ..state<InHell>((b) => b..state<Christian>((b) => b..state<Catholic>((b) => b)..state<SalvationArmy>((b) => b))))
    ..onTransition((td) => watcher.log('${td.eventType}')));

In the above code block the indentations show the level of nesting. You can see that the state 'Young' is nested within the state 'Alive'.

Each box represents a State and its nested states. In the above diagram we have two top level states 'Alive' and 'Dead'.

You can see how the 'Young' state is nested within the 'Alive' state box which reflects the above code.

There is no limit to the depth of nesting.

Leaf States

States that have no child states of their own are referred to as Leaf States.

When looking at a nested state diagram, the top state(s) are the root state and any states that are at the end of a branch are the leaf states. So in the above diagram Alive and Dead a root states and Buddhist and Old are leaf states.

Due to the limits of the dot diagraming tooling (or my ability to use it) the grey ovals are duplicates of the box. The State 'Alive' is also represented by a box and the grey oval 'Alive'. This is done so that transitions to the parent state can be easily represented on the diagram.

Abstract States

When you create a nested set of states any state that has child states becomes an 'abstract' state.

You cannot transition to an abstract state!

In the above example each of 'the Alive', 'Dead', 'InHeavan', 'InHell' and 'Christian' states have children states and are therefore abstract states.

It is only valid to create a transition to a Leaf state.

A Nested FSM can be in multiple states!

When an FSM is in a Nested State we say that it is also in ALL ancestor states.

A classic FSM can only ever be in a single state, Nested States make life more interesting >:) *2

As an example; if the above FSM transitions to the 'Catholic' state we say that the FSM is in the 'Catholic' state, 'InHell' state and the 'Dead' state simultaneously.

If you call StateMachine.isInState<Dead>() or StateMachine.isInState<InHell>() both will return true.

Cascading Events

If a parent state has a transition then we say that the child state also has that transition.

Let's look at a toaster oven that supports toasting and baking and that turns off the heater when the door is open.*1

StateMachine _createMachine() {
  return StateMachine.create((g) => g
    ..initialState<DoorOpen>()
    ..state<DoorOpen>((b) {})
    ..state<Toasting>((b) => b
            // one
           ..on<OpenDoor, DoorOpen>())
    ..state<Baking>((b) => b
            // and duplicate
           ..on<OpenDoor, DoorOpen>())
  )); 
}

In the above example the transition ..on<OpenDoor, DoorOpen> is duplicated for both the Toasting and Baking state.

If we create a super state Heating we can then make it the parent of Toasting and Baking .

We can now attached the .on<OpenDoor, DoorOpen> transition to the Heating state.

Both Toasting and Baking now inherit the .on<OpenDoor, DoorOpen> transition from the Heating state.

This is the result:

StateMachine _createMachine() {
  return StateMachine.create((g) => g
    ..initialState<DoorOpen>()
    ..state<DoorOpen>((b) {})
    // new super state
    ..state<Heating>((b) => b
      // transition shared by heating, toasting and baking.
      ..on<OpenDoor, DoorOpen>()
      ..state<Toasting>((b) {})
      ..state<Baking>((b) {}))); 
}

This may not seem much of a saving but when you have an FSM with a significant no. of States and events then Nested States significantly reduce the complexity of an FSM.

*1 Example sourced from: https://www.embedded.com/a-crash-course-in-uml-state-machines-part-2/

*2 Evil grin

Last updated