ExMachine.Statechart (ex_machine v0.1.3)

View Source

Module for building and validating statechart definitions.

A Statechart is a compiled representation of a hierarchical state machine definition. It provides the foundation for creating and running state machines with complex hierarchical structures, transitions, actions, and guards.

Statechart Structure

A compiled statechart contains:

  • states - A flattened map of all states in the hierarchy with their full paths

The statechart compiler takes a nested State definition and:

  1. Validates the state machine structure
  2. Flattens the hierarchy into addressable state paths
  3. Validates all transitions reference valid states
  4. Ensures there are no duplicate state names in the same scope
  5. Verifies initial states are valid

State Addressing

States in a statechart are addressed using dot notation paths:

  • "root" - The root state
  • "root.playing" - A top-level state called "playing"
  • "root.playing.normal_speed" - A nested state "normal_speed" inside "playing"

Configuration

The active configuration represents which states are currently active. Due to hierarchical composition, multiple states can be active simultaneously.

For example, if a media player is in "normal speed" mode:

  • Configuration: [["normal_speed", "playing", "root"]]
  • Active states: "normal_speed", "playing", and "root"

Building a Statechart

# Define your state machine structure
definition = %State{
  initial: "idle",
  substates: %{
    "idle" => %State{transitions: %{"start" => "running"}},
    "running" => %State{transitions: %{"stop" => "idle"}}
  }
}

# Compile into a statechart
statechart = Statechart.build(definition)

Validation

The build process performs comprehensive validation:

  • All referenced states must exist
  • Initial states must be valid substates
  • No duplicate state names in the same scope
  • Transitions must reference valid target states
  • State hierarchy must be well-formed

If validation fails, specific exceptions are raised describing the problem.

Usage with using macro

You can also define statecharts using the __using__ macro:

defmodule MyMachine do
  use ExMachine.Statechart

  def definition do
    %State{
      # ... your state definition
    }
  end
end

Summary

Functions

Build and return a Statechart struct that contains the compiled and validated version of the statechart in definition argument, ready to be executed in a Machine.

Return the Least Common Compound Ancestor of states list.

Returns the ancestors of state (parent of state, parent of parent, etc), in form of a list of string, ordered from nearest parent to (and always) the "root" state.

Returns the ancestors of state (parent of state, parent of parent, etc), in form of a list of string, ordered from nearest parent to (and excluded) the until state.

Returns the descendants of state, in form of an unordered MapSet of string, containing all the descendants (children, children of children, etc) of state.

Returns a list of states that must be entered when the machine is entering in the state target, considering the lcca

Return the list of enter actions for each state in list states_list, if defined for the state, in the same exact order of states_list

Return the list of exit actions for each state in list states_list, if defined for the state, in the same exact order of states_list

Returns a list of states that must be exited when the machine is exiting from the state source, considering the lcca

Return list of initial states from argument state deep to a leaf state.

Return a transition map if state have a transition defined to handle event, otherwise nil

Types

t()

@type t() :: %ExMachine.Statechart{states: map()}

Functions

build(definition)

Build and return a Statechart struct that contains the compiled and validated version of the statechart in definition argument, ready to be executed in a Machine.

During compilation, Statechart verifies that the definition is valid and raise an exception if there is a problem.

Examples

iex> eng = Statechart.build(%State{initial: "s1", substates: %{ "s1" => %State{}}})
iex> Enum.count(eng.states)
2

iex> Statechart.build("invalid")
** (ExMachine.Statechart.InvalidDefinition) Definition "invalid" is not valid

iex> defs = %State{initial: "invalid_state", substates: %{ "s1" => %State{}}}
iex> Statechart.build(defs)
** (ExMachine.Statechart.NotValidInitial) Initial state "invalid_state" is not valid or not a descendant of composite state "root"

find_lcca(statechart, states)

Return the Least Common Compound Ancestor of states list.

LCCA of a list states is the lowest (i.e. deepest) state in the state hierarchy that has all state in states as descendants.

In other words LCCA is the state s such that s is a ancestor of all states on states list and no descendants of s has this property.

Return nil if in the states list is present the root state because can't exist a state that is parent of root state.

Note that since we are speaking of ancestor (parent or parent of a parent, etc.) the LCCA is never a member of state list.

get_ancestors(statechart, state_name)

Returns the ancestors of state (parent of state, parent of parent, etc), in form of a list of string, ordered from nearest parent to (and always) the "root" state.

Examples

iex> defs = %State{initial: "s1", substates: %{ "s1" => %State{ initial: "s11", substates: %{ "s11" => %State{}}}}}
iex> eng = Statechart.build(defs)
iex> Statechart.get_ancestors(eng, "s11")
["s1", "root"]

get_ancestors_until(statechart, state_name, until)

Returns the ancestors of state (parent of state, parent of parent, etc), in form of a list of string, ordered from nearest parent to (and excluded) the until state.

Examples

iex> defs = %State{initial: "s1", substates: %{ "s1" => %State{ initial: "s11", substates: %{ "s11" => %State{}}}}}
iex> eng = Statechart.build(defs)
iex> Statechart.get_ancestors_until(eng, "s11", "root")
["s1"]

get_descendants(statechart, state_name)

Returns the descendants of state, in form of an unordered MapSet of string, containing all the descendants (children, children of children, etc) of state.

Examples

iex> defs = %State{initial: "s1", substates: %{ "s1" => %State{ initial: "s11", substates: %{ "s11" => %State{}}}}}
iex> eng = Statechart.build(defs)
iex> Statechart.get_descendants(eng, "root")
MapSet.new(["s1", "s11"])

get_entering_states(statechart, target, lcca)

Returns a list of states that must be entered when the machine is entering in the state target, considering the lcca

get_entry_actions(statechart, states_list)

Return the list of enter actions for each state in list states_list, if defined for the state, in the same exact order of states_list

get_exit_actions(statechart, states_list)

Return the list of exit actions for each state in list states_list, if defined for the state, in the same exact order of states_list

get_exiting_states(statechart, source, lcca)

Returns a list of states that must be exited when the machine is exiting from the state source, considering the lcca

get_initials(statechart, state)

Return list of initial states from argument state deep to a leaf state.

The function uses :initial key in the %State{} definition, unless it encounter a history state (to be implemented)

## Examples

iex> defs = %State{initial: "s1", substates: %{ "s1" => %State{ initial: "s11", substates: %{ "s11" => %State{}}}}}
iex> eng = Statechart.build(defs)
iex> Statechart.get_initials(eng, "root")
["root", "s1", "s11"]
iex> Statechart.get_initials(eng, "s1")
["s1", "s11"]

get_transition_for(statechart, state, event)

Return a transition map if state have a transition defined to handle event, otherwise nil

new()