View Source Protean.Action behaviour (Protean v0.0.1)
Protean manages state, processes, and side-effects through actions, data structures describing things that should occur as a result of a transition.
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.
The execution of an action can change the state of the machine (it's context, for example), or run some side-effect (like broadcasting a PubSub message). Action execution can additionally produce more actions that will be executed immediately before moving onto the next top-level action as described above.
high-level-api
High-level API
The most common way to use actions is through the Protean.action/3
callback. This callback
is run when an action is specified like this:
[
# ...
on: [
{:some_event, actions: [:first_action, :second_action]}
]
]
These are then handled in callbacks:
@impl Protean
def action(:first_action, state, _event) do
# ...
state
end
def action(:second_action, state, _event) do
# ...
state
end
Action callbacks must always return the machine state, but they can attach actions to that state that will be immediately executed by the interpreter. For instance:
def action(:update_data, state, {:data_updated, changes}) do
state
|> 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 :context
.
But, we could perform additional actions if we wish, such as:
def action(:update_data, state, {:data_updated, changes}) do
%{topic: topic, other_process: pid, data: data} = state.context
new_data = Map.merge(data, changes)
PubSub.broadcast!(@pubsub, topic, {:data_updated, changes})
state
|> Action.send({:data_commited, new_data}, to: pid)
|> Action.assign(:data, new_data)
end
Note: Best practice is to let the interpreter execute as many actions as possible, as opposed to performing explicit side-effects in the callback. This is because actions can potentially halt the execution of any remaining actions, but side-effects executed in the callback will occur before any of the returned actions can be executed.
Protean.Action
provides a number of helpers for specifying actions. See individual function
documentation for more.
low-level-api-the-action-behaviour
Low-level API: the action behaviour
Ultimately, all actions are resolving to t:action()
, a two-element tuple of a module and an
argument. The module is expected to implement the Protean.Action
behaviour (see "Callbacks").
Many of Protean's more "dynamic" features boil down to syntax sugar over actions, including
:invoke
and delayed transitions using :after
. Modules implementing the action behaviour
have direct access to the interpreter and therefore must be careful, well, not to muck anything
up.
Link to this section Summary
Types
An action is a 2-element tuple, where handler
implements the Protean.Action
behaviour and
action_arg
is an argument that will be passed back to the handler during execution.
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
An action is a 2-element tuple, where handler
implements the Protean.Action
behaviour and
action_arg
is an argument that will be passed back to the handler during execution.
@type exec_action_return() :: {:cont, Protean.Interpreter.t()} | {:cont, Protean.Interpreter.t(), [action()]} | {:halt, Protean.Interpreter.t()}
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
Link to this section Functions
TODO
TODO
TODO
TODO
TODO
@spec exec(action() | term(), Protean.Interpreter.t()) :: exec_action_return()
Executes an action given an interpreter. See exec_action/2
.
@spec put(Protean.State.t(), action()) :: Protean.State.t()
TODO
TODO