Elixir GenStateM Basics
A state machine is a process that sits in one named state and moves to another state on each event. Its callback module names the states and answers every event.
Start a machine and call it
A gate starts closed. An open call moves it to open and answers open.
defmodule Gate do
@behaviour :gen_statem
def callback_mode(), do: :handle_event_function
def init(:ok), do: {:ok, :closed, 0}
def handle_event({:call, from}, :open, :closed, data) do
{:next_state, :open, data + 1, [{:reply, from, :open}]}
end
def handle_event({:call, from}, :status, state, _data) do
{:keep_state_and_data, [{:reply, from, state}]}
end
def handle_event({:call, from}, _event, _state, _data) do
{:keep_state_and_data, [{:reply, from, :ignored}]}
end
def handle_event(:cast, _event, _state, _data) do
:keep_state_and_data
end
end
{:ok, pid} = :gen_statem.start_link(Gate, :ok, [])
IO.inspect(:gen_statem.call(pid, :status))
IO.inspect(:gen_statem.call(pid, :open))
IO.inspect(:gen_statem.call(pid, :status))
:ok = :gen_statem.stop(pid)Cast without waiting, then check with a call
A close cast moves an open gate to closed without a reply. A later status call proves the move.
defmodule Gate do
@behaviour :gen_statem
def callback_mode(), do: :handle_event_function
def init(:ok), do: {:ok, :closed, 0}
def handle_event({:call, from}, :open, :closed, data) do
{:next_state, :open, data + 1, [{:reply, from, :open}]}
end
def handle_event({:call, from}, :status, state, _data) do
{:keep_state_and_data, [{:reply, from, state}]}
end
def handle_event({:call, from}, _event, _state, _data) do
{:keep_state_and_data, [{:reply, from, :ignored}]}
end
def handle_event(:cast, :close, :open, data) do
{:next_state, :closed, data}
end
def handle_event(:cast, _event, _state, _data) do
:keep_state_and_data
end
end
{:ok, pid} = :gen_statem.start_link(Gate, :ok, [])
IO.inspect(:gen_statem.call(pid, :open))
:gen_statem.cast(pid, :close)
IO.inspect(:gen_statem.call(pid, :status))
:ok = :gen_statem.stop(pid)Data counts events across states
Each opening adds one to the count held beside the state. A count call reads the total without moving the machine.
defmodule Gate do
@behaviour :gen_statem
def callback_mode(), do: :handle_event_function
def init(:ok), do: {:ok, :closed, 0}
def handle_event({:call, from}, :open, :closed, data) do
{:next_state, :open, data + 1, [{:reply, from, :open}]}
end
def handle_event({:call, from}, :opens, _state, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
def handle_event({:call, from}, :status, state, _data) do
{:keep_state_and_data, [{:reply, from, state}]}
end
def handle_event({:call, from}, _event, _state, _data) do
{:keep_state_and_data, [{:reply, from, :ignored}]}
end
def handle_event(:cast, :close, :open, data) do
{:next_state, :closed, data}
end
def handle_event(:cast, _event, _state, _data) do
:keep_state_and_data
end
end
{:ok, pid} = :gen_statem.start_link(Gate, :ok, [])
IO.inspect(:gen_statem.call(pid, :open))
:gen_statem.cast(pid, :close)
IO.inspect(:gen_statem.call(pid, :open))
IO.inspect(:gen_statem.call(pid, :opens))
:ok = :gen_statem.stop(pid)Example
defmodule Gate do
@behaviour :gen_statem
def callback_mode(), do: :handle_event_function
def init(:ok), do: {:ok, :closed, 0}
def handle_event({:call, from}, :status, state, _data) do
{:keep_state_and_data, [{:reply, from, state}]}
end
def handle_event({:call, from}, :opens, _state, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
def handle_event({:call, from}, :open, :closed, data) do
{:next_state, :open, data + 1, [{:reply, from, :open}]}
end
def handle_event(:cast, :close, :open, data) do
{:next_state, :closed, data}
end
def handle_event({:call, from}, _event, _state, _data) do
{:keep_state_and_data, [{:reply, from, :ignored}]}
end
def handle_event(:cast, _event, _state, _data) do
:keep_state_and_data
end
end
{:ok, pid} = :gen_statem.start_link(Gate, :ok, [])
:gen_statem.cast(pid, :close)
IO.inspect(:gen_statem.call(pid, :status))
IO.inspect(:gen_statem.call(pid, :open))
IO.inspect(:gen_statem.call(pid, :open))
:gen_statem.cast(pid, :close)
IO.inspect(:gen_statem.call(pid, :status))
IO.inspect(:gen_statem.call(pid, :opens))
:ok = :gen_statem.stop(pid)
IO.puts("stopped")Key points
- State machine: a process with one current state and rules for moving between states.
- Use :gen_statem.start_link/3 with a callback module to start a state machine.
- Use :gen_statem.call/2 to send an event and wait for a reply.
- Use :gen_statem.cast/2 to send an event without waiting for a reply.
- The callback module answers events in handle_event/4, naming the next state and the new data.
- Use :gen_statem.stop/1 to stop a state machine when it is no longer needed.
In this example, a close cast while the gate is closed is ignored, so the status call still answers closed. An open call moves the gate to open and counts one opening, while a second open call is ignored. A close cast moves it back to closed, and the final calls confirm the closed state and the count of one opening.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A gate is open. What does a second :open call answer?