This section is to be read with the
This is a new behaviour in OTP-19.0. It has been thoroughly reviewed, is stable enough to be used by at least two heavy OTP applications, and is here to stay. But depending on user feedback, we do not expect but might find it necessary to make minor not backwards compatible changes into OTP-20.0, so its state can be designated as "not quite experimental"...
Established Automata theory does not deal much with how a state transition is triggered, but in general assumes that the output is a function of the input (and the state) and that they are some kind of values.
For an Event Driven State Machine the input is an event that triggers a state transition and the output is actions executed during the state transition. It can analogously to the mathematical model of a Finite State Machine be described as a set of relations of the form:
State(S) x Event(E) -> Actions(A), State(S')
These relations are interpreted as meaning:
If we are in state
Note that
Since
Like most
The
StateName(EventType, EventContent, Data) -> .. code for actions here ... {next_state, NewStateName, NewData}.
In the mode
handle_event(EventType, EventContent, State, Data) -> .. code for actions here ... {next_state, State', Data'}
Both these modes allow other return tuples
that you can find in the
The two
You can for example do this by focusing on one state at the time and for every state ensure that all events are handled, or the other way around focus on one event at the time and ensure that it is handled in every state, or mix these strategies.
With
This mode fits well when you have a regular state diagram like the ones in this chapter that describes all events and actions belonging to a state visually around that state, and each state has its unique name.
With
This mode works equally well when you want to focus on
one event at the time or when you want to focus on
one state at the time, but the
The mode enables the use of non-atom states for example
complex states or even hiearchical states.
If, for example, a state diagram is largely alike
for the client and for the server side of a protocol;
then you can have a state
This is an example starting off as equivalent to the the example in the
A door with a code lock can be viewed as a state machine. Initially, the door is locked. Anytime someone presses a button, this generates an event. Depending on what buttons have been pressed before, the sequence so far can be correct, incomplete, or wrong.
If it is correct, the door is unlocked for 10 seconds (10000 ms). If it is incomplete, we wait for another button to be pressed. If it is is wrong, we start all over, waiting for a new button sequence.
We can implement such a code lock state machine using
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
button(Digit) ->
gen_statem:cast(?NAME, {button,Digit}).
init(Code) ->
do_lock(),
Data = #{code => Code, remaining => Code},
{?CALLBACK_MODE,locked,Data}.
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] ->
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{next_state,locked,Data#{remaining := Rest}};
_Wrong ->
{next_state,locked,Data#{remaining := Code}}
end.
open(timeout, _, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
do_lock(),
{next_state,locked,Data}.
do_lock() ->
io:format("Lock~n", []).
do_unlock() ->
io:format("Unlock~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
]]>
The code is explained in the next sections.
In the example in the previous section, the
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
]]>
The first argument,
If the name is omitted, the
The second argument,
The interface functions (
The third argument,
The fourth argument,
If name registration succeeds, the new
do_lock(),
Data = #{code => Code, remaining => Code},
{?CALLBACK_MODE,locked,Data}.
]]>
The function notifying the code lock about a button event is
implemented using
gen_statem:cast(?NAME, {button,Digit}).
]]>
The first argument is the name of the
The event is made into a message and sent to the
case Remaining of
[Digit] -> % Complete
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{next_state,locked,Data#{remaining := Rest}};
[_|_] -> % Wrong
{next_state,locked,Data#{remaining := Code}}
end.
open(timeout, _, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
do_lock(),
{next_state,locked,Data}.
]]>
If the door is locked and a button is pressed, the pressed
button is compared with the next correct button and,
depending on the result, the door is either unlocked
and the
If the pressed button is incorrect the server data restarts from the start of the code sequence.
In state
When a correct code has been given, the door is unlocked and
the following tuple is returned from
10000 is a time-out value in milliseconds.
After this time, that is; 10 seconds, a time-out occurs.
Then,
do_lock(),
{next_state,locked,Data};
]]>
Sometimes an event can arrive in any state of the
Let's introduce a
gen_statem:call(?NAME, code_length).
...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code} = Data) ->
{keep_state,Data,[{reply,From,length(Code)}]}.
]]>
This example uses
If you use the mode
case State of
locked ->
case maps:get(remaining, Data) of
[Digit] -> % Complete
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
open ->
do_lock(),
{next_state,locked,Data}
end;
handle_event(timeout, _, open, Data) ->
do_lock(),
{next_state,locked,Data}.
...
]]>
If the
If it is necessary to clean up before termination, the shutdown
strategy must be a time-out value and the
process_flag(trap_exit, true),
do_lock(),
...
]]>
In this example we let the
State =/= locked andalso do_lock(),
ok.
]]>
If the
gen_statem:stop(?NAME).
]]>
This makes the
In the first chapters we mentioned actions as a part of
the general state machine model, and these actions
are implemented with the code the
There are more specific state transition actions
that a callback function can order the
We have mentioned the event timeout
and replying to a caller in the example above.
An example of event postponing comes in later in this chapter.
See the
So far we have mentioned a few
Here is the complete list of event types and where they come from:
The timeout event generated by the state transition action
Often you want a timer to not be cancelled by any event
or you want to start a timer in one state and respond
to the timeout in another. This can be accomplished
with a regular erlang timer:
Looking at the example in this chapter so far; using the
Suppose we do not want a button to lock the door,
instead we want to ignore button events in the
case Remaining of
[Digit] ->
do_unlock(),
Tref = erlang:start_timer(10000, self(), lock),
{next_state,open,Data#{remaining := Code, timer := Tref}};
...
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
]]>
If you need to cancel a timer due to some other event you can use
Another way to cancel a timer is to not cancel it, but instead to ignore it if it arrives in a state where it is known to be late.
If you want to ignore a particular event in the current state
and handle it in a future state, you can postpone the event.
A postponed event is retried after the state has
changed i.e
Postponing is ordered by the state transition
In this example, instead of ignoring button events
while in the
{keep_state,Data,[postpone]};
...
]]>
The fact that a postponed event is only retried after a state change
translates into a requirement on the event and state space:
if you have a choice between storing a state data item
in the
What you want to avoid is that you maybe much later decide
to postpone an event in one state and by misfortune it is never retried
because the code only changes the
It is not uncommon that a state diagram does not specify how to handle events that are not illustrated in a particular state in the diagram. Hopefully this is described in an associated text or from the context.
Possible actions may be; ignore as in drop the event (maybe log it) or deal with the event in some other state as in postpone it.
Erlang's selective receive statement is often used to describe simple state machine examples in straightforward Erlang code. Here is a possible implementation of the first example:
spawn(
fun () ->
true = register(?NAME, self()),
do_lock(),
locked(Code, Code)
end).
button(Digit) ->
?NAME ! {button,Digit}.
locked(Code, [Digit|Remaining]) ->
receive
{button,Digit} when Remaining =:= [] ->
do_unlock(),
open(Code);
{button,Digit} ->
locked(Code, Remaining);
{button,_} ->
locked(Code, Code)
end.
open(Code) ->
receive
after 10000 ->
do_lock(),
locked(Code, Code)
end.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
]]>
The selective receive in this case causes
A selective receive can not be used from a
The state transition
Other than that both mechanisms have got the same theoretical time and memory complexity, while the selective receive language construct has got smaller constant factors.
It may be beneficial in some cases to be able to generate events
to your own state machine.
This can be done with the state transition
You can generate events of any existing
One example of using self generated events may be when you have
a state machine specification that uses state entry actions.
That you could code using a dedicated function
to do the state transition. But if you want that code to be
visible besides the other state logic you can insert
an
Here is an implementation of entry actions
using
process_flag(trap_exit, true),
Data = #{code => Code},
enter(?CALLBACK_MODE, locked, Data).
...
locked(internal, enter, _Data) ->
do_lock(),
{keep_state,Data#{remaining => Code}};
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] ->
enter(next_state, open, Data);
...
open(internal, enter, _Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
...
enter(Tag, State, Data) ->
{Tag,State,Data,[{next_event,internal,enter}]}.
]]>
Here is the example after all mentioned modifications and some more utilizing the entry actions, which deserves a new state diagram:
Note that this state diagram does not specify how to handle
a button event in the state
Using state functions:
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->
gen_statem:stop(?NAME).
button(Digit) ->
gen_statem:cast(?NAME, {button,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
init(Code) ->
process_flag(trap_exit, true),
Data = #{code => Code},
enter(?CALLBACK_MODE, locked, Data).
locked(internal, enter, #{code := Code} = Data) ->
do_lock(),
{keep_state,Data#{remaining => Code}};
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] -> % Complete
enter(next_state, open, Data);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
locked(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
open(internal, enter, Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
open(cast, {button,_}, _) ->
{keep_state_and_data,[postpone]};
open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code}) ->
{keep_state_and_data,[{reply,From,length(Code)}]}.
enter(Tag, State, Data) ->
{Tag,State,Data,[{next_event,internal,enter}]}.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
]]>
What to change to use one
do_lock(),
{keep_state,Data#{remaining => Code}};
handle_event(
cast, {button,Digit}, locked,
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] -> % Complete
enter(next_state, open, Data);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
%%
%% State: open
handle_event(internal, enter, open, Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
handle_event(cast, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
%%
%% Any state
handle_event({call,From}, code_length, _State, #{code := Code}) ->
{keep_state_and_data,[{reply,From,length(Code)}]}.
...
]]>
Note that postponing buttons from the
The example servers so far in this chapter will for example when killed by an exit signal or due to an internal error print out the full internal state in the error log. This state contains both the code lock code and which digits that remains to unlock.
This state data can be regarded as sensitive, and maybe not what you want in the error log because of something unpredictable happening.
Another reason to filter the state can be that the state is too big to print out since it fills the error log with uninteresting details.
To avoid this you can format the internal state
that gets in the error log and gets returned from
StateData =
{State,
maps:filter(
fun (code, _) -> false;
(remaining, _) -> false;
(_, _) -> true
end,
Data)},
case Opt of
terminate ->
StateData;
normal ->
[{data,[{"State",StateData}]}]
end.
]]>
It is not mandatory to implement a
The callback mode
One reason to use this is when you have
a state item that affects the event handling
in particular when combining that with postponing events.
Let us complicate the previous example
by introducing a configurable lock button
(this is the state item in question)
that in the
Suppose now that we call
So let us make the
If now one process calls
We define the state as
gen_statem:start_link(
{local,?NAME}, ?MODULE, {Code,LockButton}, []).
stop() ->
gen_statem:stop(?NAME).
button(Digit) ->
gen_statem:call(?NAME, {button,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
set_lock_button(LockButton) ->
gen_statem:call(?NAME, {set_lock_button,LockButton}).
init({Code,LockButton}) ->
process_flag(trap_exit, true),
Data = #{code => Code, remaining => undefined, timer => undefined},
enter(?CALLBACK_MODE, {locked,LockButton}, Data, []).
handle_event(
{call,From}, {set_lock_button,NewLockButton},
{StateName,OldLockButton}, Data) ->
{next_state,{StateName,NewLockButton},Data,
[{reply,From,OldLockButton}]};
handle_event(
{call,From}, code_length,
{_StateName,_LockButton}, #{code := Code}) ->
{keep_state_and_data,
[{reply,From,length(Code)}]};
handle_event(
EventType, EventContent,
{locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->
case {EventType,EventContent} of
{internal,enter} ->
do_lock(),
{keep_state,Data#{remaining := Code}};
{{call,From},{button,Digit}} ->
case Remaining of
[Digit] -> % Complete
next_state(
{open,LockButton}, Data,
[{reply,From,ok}]);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest},
[{reply,From,ok}]};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code},
[{reply,From,ok}]}
end
end;
handle_event(
EventType, EventContent,
{open,LockButton}, #{timer := Timer} = Data) ->
case {EventType,EventContent} of
{internal,enter} ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer := Tref}};
{info,{timeout,Timer,lock}} ->
next_state({locked,LockButton}, Data, []);
{{call,From},{button,Digit}} ->
if
Digit =:= LockButton ->
erlang:cancel_timer(Timer),
next_state(
{locked,LockButton}, Data,
[{reply,From,locked}]);
true ->
{keep_state_and_data,
[postpone]}
end
end.
next_state(State, Data, Actions) ->
enter(next_state, State, Data, Actions).
enter(Tag, State, Data, Actions) ->
{Tag,State,Data,[{next_event,internal,enter}|Actions]}.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
format_status(Opt, [_PDict,State,Data]) ->
StateData =
{State,
maps:filter(
fun (code, _) -> false;
(remaining, _) -> false;
(_, _) -> true
end,
Data)},
case Opt of
terminate ->
StateData;
normal ->
[{data,[{"State",StateData}]}]
end.
]]>
It may be an ill-fitting model for a physical code lock
that the