kevin-hanselman / elixir_statemachine

An educational hierarchical state machine library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Elixir State Machine

A simple hierarchical state machine library in Elixir. Written primarily for educational purposes.

Usage

Here's an example state machine for a microwave. Nested modules create hierarchical states.

defmodule Microwave do
  defstruct time: 0
  use State

  require Logger

  defmodule Idle do
    use State

    def enter(data) do
      Logger.info("light off")
      data
    end

    def on_event({:key_press, num}, %Microwave{time: t}) when is_number(num) do
      {nil, %Microwave{time: 10 * t + num}}
    end

    # Returning nil for the next state means no transition, a.k.a. an "internal
    # transition".
    def on_event({:key_press, :stop_clear}, _data), do: {nil, %Microwave{time: 0}}

    def on_event({:key_press, :start}, data), do: {Microwave.Running, data}

    defmodule DoorOpen do
      use State

      def enter(data) do
        Logger.info("light on")
        data
      end

      def on_event({:key_press, :start}, data), do: {nil, data}

      def on_event(:close_door, data), do: {Microwave.Idle, data}
    end
  end

  defmodule Running do
    use State

    def enter(data) do
      Logger.info("light on")
      Logger.info("emitter on")
      data
    end

    def exit(data) do
      Logger.info("emitter off")
      data
    end

    def on_event(:tick, %Microwave{time: 1}) do
      Logger.info("done cooking")
      {Microwave.Idle, %Microwave{}}
    end

    def on_event(:tick, %Microwave{time: t}), do: {nil, %Microwave{time: t - 1}}

    def on_event({:key_press, :stop_clear}, data), do: {Microwave.Idle, data}
  end

  def init, do: {Idle, %__MODULE__{}}

  def on_event(:open_door, data), do: {Idle.DoorOpen, data}
end

Instantiating the state machine is as easy as starting a StateMachine GenServer with the root State module as the initial argument. Then you can send the state machine events using the GenServer API.

# start a Microwave state machine
{:ok, sm} = GenServer.start_link(StateMachine, Microwave)

# print all events and resulting state machine state
:sys.trace(sm, true)

# send events to the microwave
:ok = GenServer.call(sm, {:key_press, 1})

:ok = GenServer.call(sm, {:key_press, 2})

:ok = GenServer.call(sm, {:key_press, :start})

:ok = GenServer.call(sm, :tick)

:ok = GenServer.call(sm, :open_door)

Known bugs/deficiencies

Self-transitions are always external, other transitions are always local. See Wikipedia for the difference.

No support for orthogonal regions or deferred events.

Installation

def deps do
  [
    {:statemachine, git: "https://github.com/kevin-hanselman/elixir_statemachine.git", tag: "main"}
  ]
end

About

An educational hierarchical state machine library

License:MIT License


Languages

Language:Elixir 100.0%