This section is to be read with the
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, StateName', Data'}.
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
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,code_lock}, ?MODULE, Code, []).
button(Digit) ->
gen_statem:cast(code_lock, {button,Digit}).
init(Code) ->
do_lock(),
Data = #{code => Code, remaining => Code},
{state_functions,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) ->
{ok,State,Data}.
]]>
The code is explained in the next sections.
In the example in the previous section, the
gen_statem:start_link({local,code_lock}, ?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},
{state_functions,locked,Data}.
]]>
The function notifying the code lock about a button event is
implemented using
gen_statem:cast(code_lock, {button,Digit}).
]]>
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(code_lock, 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
Data = #{code => Code, remaining => Code},
{handle_event_function,locked,Data}.
handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->
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),
...
{CallbackMode,State,Data}.
]]>
In this example we let the
State =/= locked andalso do_lock(),
ok.
]]>
If the
gen_statem:stop(code_lock).
]]>
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
In this example, instead of ignoring button events
while in the
{keep_state,Data,[postpone]};
...
]]>
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(code_lock, self()),
do_lock(),
locked(Code, Code)
end).
button(Digit) ->
code_lock ! {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
The
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
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
Data = #{code => Code},
enter(state_functions, 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,code_lock}, ?MODULE, Code, []).
stop() ->
gen_statem:stop(code_lock).
button(Digit) ->
gen_statem:cast(code_lock, {button,Digit}).
code_length() ->
gen_statem:call(code_lock, code_length).
init(Code) ->
Data = #{code => Code},
enter(state_functions, 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) ->
{ok,State,Data}.
]]>
What to change to use one
process_flag(trap_exit, true),
Data = #{code => Code},
enter(handle_event_function, locked, Data).
...
%% State: locked
handle_event(internal, enter, locked, #{code := Code} = Data) ->
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