state_server v0.4.0 StateServer.State behaviour View Source

A behaviour that lets you organize code for your StateServer states.

Organization

When you define your StateServer, the StateServer module gives you the opportunity to define state modules. These are typically (but not necessarily) submodules scoped under the main StateServer module. In this way, your code for handling events can be neatly organized by state. In some (but not all) cases, this may be the most appropriate way to keep your state machine codebase sane.

Defining the state module.

the basic syntax for defining a state module is as follows:

defstate MyModule, for: :state do
  # ... code goes here ...

  def handle_call(:increment, _from, data) do
    {:reply, :ok, update: data + 1}
  end
end

note that the callback directives defined in this module are identical to those of StateServer, except that they are missing the state argument.

External state modules

You might want to use an external module to handle event processing for one your state machine. Reasons might include:

  • to enable code reuse between state machines
  • if your codebase is getting too long and you would like to put state modules in different files.

If you choose to do so, there is a short form defstate call, which is as follows:

defstate ExternalModule, for: :state

Precedence and Defer statements

Note that handle_* functions written directly in the body of the StateServer take precedence over any functions written as a part of a state module. In the case where there are competing function calls, your handler functions written in the body of the StateServer may emit :defer as a result, which will punt the processing of the event to the state modules.

# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
  {:reply, {state, data}}
end
# for all other call payloads, send to the state modules
def handle_call(_, _, _, _) do
  :defer
end

defstate Start, for: :start do
  def handle_call(...) do...

since this is a common pattern, we provide a defer macro which is equivalent to the above.

# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
  {:reply, {state, data}}
end
# for all other call payloads, send to the state modules
defer handle_call

You do not need this pattern for event handlers which are not implemented in the body of the function.

Example

The following code should produce a "light switch" state server that announces when it's been flipped.

defmodule SwitchWithStates do

  @doc """
  implements a light switch as a state server.  In data, it keeps a count of
  how many times the state of the light switch has changed.

  On transition, it sends to standard error a comment that it has been flipped.
  Note that the implementations are different between the two states.
  """

  use StateServer, off: [flip: :on],
                   on:  [flip: :off]

  @type data :: non_neg_integer

  def start_link, do: StateServer.start_link(__MODULE__, :ok)

  @impl true
  def init(:ok), do: {:ok, 0}

  def flip(srv), do: StateServer.call(srv, :flip)
  def query(srv), do: StateServer.call(srv, :query)

  @impl true
  def handle_call(:flip, _from, _state, _count) do
    {:reply, :ok, transition: :flip}
  end
  defer handle_call
  # we must defer the handle_call statement because there are both shared and
  # individual implementation of handle_call features.

  defstate Off, for: :off do
    @impl true
    def handle_transition(:flip, count) do
      IO.puts(:stderr, "switch #{inspect self()} flipped on, #{count} times turned on")
      {:noreply, update: count + 1}
    end

    @impl true
    def handle_call(:query, _from, _count) do
      {:reply, "state is off"}
    end
  end

  defstate On, for: :on do
    @impl true
    def handle_transition(:flip, count) do
      IO.puts(:stderr, "switch #{inspect self()} flipped off, #{count} times turned on")
      :noreply
    end

    @impl true
    def handle_call(:query, _from, _count) do
      {:reply, "state is on"}
    end
  end

end

Link to this section Summary

Link to this section Callbacks

Link to this callback

handle_call(term, from, term)

View Source (optional)
handle_call(term(), from(), term()) ::
  reply_response() | noreply_response() | stop_response()
Link to this callback

handle_cast(term, term)

View Source (optional)
handle_cast(term(), term()) :: noreply_response() | stop_response()
Link to this callback

handle_continue(term, term)

View Source (optional)
handle_continue(term(), term()) :: noreply_response() | stop_response()
Link to this callback

handle_info(term, term)

View Source (optional)
handle_info(term(), term()) :: noreply_response() | stop_response()
Link to this callback

handle_internal(term, term)

View Source (optional)
handle_internal(term(), term()) :: noreply_response() | stop_response()
Link to this callback

handle_timeout(term, term)

View Source (optional)
handle_timeout(term(), term()) :: noreply_response() | stop_response()
Link to this callback

handle_transition(atom, term)

View Source (optional)
handle_transition(atom(), term()) ::
  noreply_response() | stop_response() | :cancel
Link to this callback

on_state_entry(atom, term)

View Source (optional)
on_state_entry(atom(), term()) :: StateServer.on_state_entry_response()