View Source Protean.Action behaviour (Protean v0.1.0-alpha.1)
Protean manages state, processes, and side-effects through actions, data structures describing things that should occur as a result of a transition.
introduction
Introduction
When a Protean machine transitions from one state to the next (or even self-transitions back to the same state), the interpreter collects any actions that should be performed as a result of that transition. Actions are then executed in a specific order:
- Exit actions - Any
:exit
actions specified on states that are exiting as a result of the transition. - Transition actions - Any
:actions
specified on the transition that responded to the event. - Entry actions - Any
:entry
actions specified on the states being entered.
Actions are a unifying abstraction that serve many purposes:
- assign to and update a machine's context;
- produce additional actions to be run immediately after;
- run arbitrary side-effects, like broadcasting a PubSub message;
- reply to the sender of the event with a value;
- spawn additional processes whose lifecycle can be tied to machine execution.
I recommend reading the sections below to gain a better understanding of how and where actions can be applied, then review the "Callback Actions" and "Inline Actions" provided by this module.
handling-an-action
Handling an action
The most common way to use actions is through the Protean.handle_action/3
callback.
This callback is run when an action is specified like this:
[
# ...
entry: [:entering_state]
on: [
{match(%MyEvent{}), actions: [:first_action, :second_action]}
],
exit: [:exiting_state]
]
These are then handled in callbacks:
@impl true
def handle_action(:first_action, context, %MyEvent{data: data}) do
# ...
{:reply, reply, context}
end
See Protean.handle_action/3
for possible return values and their effect.
Action callbacks must always return the machine context, but they can attach actions to that context that will be immediately executed by the interpreter. For instance:
def handle_action(:update_data, context, {:data_updated, changes}) do
context
|> Action.assign_in([:data], &Map.merge(&1, changes))
end
In this case, assign_in/3
is being used to update some data in the machine :assigns
.
But, we could perform additional actions if we wish, such as:
def handle_action(:update_data, context, {:data_updated, changes}) do
%{topic: topic, other_process: pid, data: data} = context.assigns
new_data = Map.merge(data, changes)
PubSub.broadcast!(@pubsub, topic, {:data_updated, changes})
context
|> Action.send({:data_commited, new_data}, to: pid)
|> Action.assign(:data, new_data)
end
Note about return values
These last two examples have returned the context directly, which is equivalent to returning
{:noreply, context}
. You can also emit replies that will be available to the sender of the event using{:reply, reply, context}
. This can be very useful for creating client APIs around your machines, much like you would do withGenServer
.
The best practice is to let the interpreter execute as many actions as possible, as opposed to performing explicit side-effects in the callback. In many cases, such as spawning additional processes, this allows Protean to supervise and tie process lifecycle to the machine's lifecycle (or changes in state).
low-level-api-the-action-behaviour
Low-level API: the Action behaviour
Ultimately, all actions resolve to Protean.Action.t/0
, a simple struct containing a module
and an argument. The module is expected to implement the Protean.Action
behaviour.
Action execution, then, follows a simple logic: if the action being executed is a Protean.Action
struct, call the exec_action/2
callback on the module, passing it the associated
argument and the interpreter in its current state. Otherwise, wrap it in a struct that
delegates to the callback module associated with the machine. This is how
Protean.handle_action/3
is called.
Many of features features boil down to syntax sugar over actions, including :invoke
and
delayed transitions using :after
.
Link to this section Summary
Callback Actions
Attach an action that assigns to a machine's context.
Attach an action that assigns into a machine's context.
Attach an action that executes the first of a list of actions whose guard is truthy.
Attach a custom action that implements the Protean.Action
behaviour.
Attach an action that will send a message to a process.
Inline Actions
Create an inline action that applies a function to or merges assigns into machine context.
Create an inline action that will assign to machine context.
Create an inline action that will assign into machine context.
Create an inline action that will execute the first of a list of actions whose guard is truthy.
Create an inline action that will send a message to a process.
Callbacks
Accepts the action_arg
passed with the action as well as the Protean interpreter. Returns
one of three values
Link to this section Types
@type exec_action_return() :: {:cont, Protean.Interpreter.t()} | {:cont, Protean.Interpreter.t(), [t()]} | {:halt, Protean.Interpreter.t()}
Allowed return value from exec_action/2
.
Data structure representing an executable action.
Link to this section Callback Actions
@spec assign(Protean.Context.t(), key :: term(), value :: term()) :: Protean.Context.t()
Attach an action that assigns to a machine's context.
@spec assign_in(Protean.Context.t(), [term(), ...], term()) :: Protean.Context.t()
Attach an action that assigns into a machine's context.
Similar to put_in/3
.
Attach an action that executes the first of a list of actions whose guard is truthy.
@spec put(Protean.Context.t(), t()) :: Protean.Context.t()
Attach a custom action that implements the Protean.Action
behaviour.
@spec send(Protean.Context.t(), event :: term(), [term()]) :: Protean.Context.t()
Attach an action that will send a message to a process.
Link to this section Inline Actions
Create an inline action that applies a function to or merges assigns into machine context.
@spec assign(Protean.Context.t(), assigns :: term()) :: Protean.Context.t()
@spec assign(key :: term(), value :: term()) :: t()
Create an inline action that will assign to machine context.
Create an inline action that will assign into machine context.
Similar to put_in/3
.
Create an inline action that will execute the first of a list of actions whose guard is truthy.
Create an inline action that will send a message to a process.
Link to this section Callbacks
@callback exec_action(action_arg :: term(), Protean.Interpreter.t()) :: exec_action_return()
Accepts the action_arg
passed with the action as well as the Protean interpreter. Returns
one of three values:
{:cont, interpreter}
to continue running the action pipeline{:cont, interpreter, [action]}
to inject actions that will be run immediately before running the rest of the pipeline's actions{:halt, interpreter}
to halt the action pipeline, canceling any further actions