This chapter should be read in conjunction with gen_fsm(3)
,
where all interface functions and callback functions are described
in detail.
A finite state machine, FSM, can 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
S
and the eventE
occurs, we should perform the actionsA
and make a transition to the stateS'
.
For an FSM implemented using the gen_fsm
behaviour,
the state transition rules are written as a number of Erlang
functions which conform to the following convention:
StateName(Event, StateData) -> .. code for actions here ... {next_state, StateName', StateData'}
A door with a code lock could be viewed as an FSM. Initially, the door is closed. When someone presses a button, this generates an event. If the button sequence pressed so far is the correct code, the door is opened and held open for 30 seconds (30000 milliseconds). If the button sequence is incomplete, the door remains closed and we wait for another button to be pressed. If the button sequence is wrong, we start all over, waiting for a new button sequence.
Implementing the code lock FSM using gen_fsm
results in
this callback module:
-module(code_lock). -behaviour(gen_fsm). -export([start_link/1]). -export([button/1]). -export([init/1, closed/2, open/2]). start_link(Code) -> gen_fsm:start_link({local, code_lock}, code_lock, Code, []). button(Digit) -> gen_fsm:send_event(code_lock, {button, Digit}). init(Code) -> {ok, closed, {[], Code}}. closed({button, Digit}, {SoFar, Code}) -> case [Digit|SoFar] of Code -> do_open(), {next_state, open, {[], Code}, 3000}; Incomplete when length(Incomplete)<length(Code) -> {next_state, closed, {Incomplete, Code}}; _Wrong -> {next_state, closed, {[], Code}}; end. open(timeout, State) -> do_close(), {next_state, closed, State}.
The code is explained in the next sections.
In the example in the previous section, the gen_fsm is started by
calling code_lock:start_link(Code)
:
start_link(Code) -> gen_fsm:start_link({local, code_lock}, code_lock, Code, []).
start_link
calls the function gen_fsm:start_link/4
.
This function spawns and links to a new process, a gen_fsm.
{local, code_lock}
specifies
the name. In this case, the gen_fsm will be locally registered
as code_lock
.{global, Name}
, in which case the gen_fsm is registered
using global:register_name/2
.code_lock
, is the name of
the callback module, that is the module where the callback
functions are located.start_link
and
button
) are located in the same module as the callback
functions (init
, closed
and open
). This is
normally good programming practice, to have the code
corresponding to one process contained in one module.Code
, is a term which is passed
as-is to the callback function init
. Here, init
gets the correct code for the lock as indata.gen_fsm(3)
for available options.If name registration succeeds, the new gen_fsm process calls
the callback function code_lock:init(Code)
. init
is expected to return {ok, StateName, StateData}
, where
StateName
is the name of the initial state of the gen_fsm.
In this case closed
, assuming the door is closed to begin
with. StateData
is the internal state of the gen_fsm. (For
gen_fsms, the internal state is often referred to 'state data' to
distinguish it from the state as in states of a state machine.)
In this case, the state data is the button sequence so far (empty
to begin with) and the correct code of the lock.
init(Code) -> {ok, closed, {[], Code}}.
Note that gen_fsm:start_link
is synchronous. It does not
return until the gen_fsm has been initialized and is ready to
receive notifications.
The function notifying the code lock about a button event is
implemented using gen_fsm:send_event/2
:
button(Digit) -> gen_fsm:send_event(code_lock, {button, Digit}).
code_lock
is the name of the gen_fsm and must agree with
the name used to start it. {button, Digit}
is the actual
event.
The event is made into a message and sent to the gen_fsm. When
the event is received, the gen_fsm calls
StateName(Event, StateData)
which is expected to return a
tuple {next_state, StateName1, StateData1}
.
StateName
is the name of the current state and
StateName1
is the name of the next state to go to.
StateData1
is a new value for the state data of the gen_fsm.
closed({button, Digit}, {SoFar, Code}) -> case [Digit|SoFar] of Code -> do_open(), {next_state, open, {[], Code}, 30000}; Incomplete when length(Incomplete)<length(Code) -> {next_state, closed, {Incomplete, Code}}; _Wrong -> {next_state, closed, {[], Code}}; end. open(timeout, State) -> do_close(), {next_state, closed, State}.
If the door is closed and a button is pressed, the complete
button sequence so far is compared with the correct code for
the lock and, depending on the result, the door is either opened
and the gen_fsm goes to state open
, or the door remains in
state closed
.
When a correct code has been givened, the door is opened and
the following tuple is returned from closed/2
:
{next_state, open, {[], Code}, 30000};
30000 is a timeout value in milliseconds. After 30000
milliseconds, i.e. 30 seconds, a timeout occurs. Then
StateName(timeout, StateData)
is called. In this case,
the timeout occurs when the door has been in state open
for
30 seconds and then the door is closed:
open(timeout, State) -> do_close(), {next_state, closed, State}.
Sometimes an event can arrive at any state of the gen_fsm.
Instead of sending the message with gen_fsm:send_event/2
and writing one clause handling the event for each state function,
the message can be sent with gen_fsm:send_all_state_event/2
and handled with Module:handle_event/3
:
-module(code_lock). ... -export([stop/0]). ... stop() -> gen_fsm:send_all_state_event(code_lock, stop). ... handle_event(stop, _StateName, StateData) -> {stop, normal, StateData}.
If the gen_fsm is part of a supervision tree, no stop function is
needed. The gen_fsm will automatically be terminated by its
supervisor calling exit(Pid, shutdown)
.
The gen_fsm process does not trap exit signals. If it is
necessary to do some cleaning up before termination, it should
be set to trap exit signals in the init
function and
another callback function terminate(Reason, State)
should
be implemented doing the cleaning up:
init(Args) -> ..., process_flag(trap_exit, true), ..., {ok, StateName, StateData}. ... terminate(shutdown, StateName, StateData) -> ..code for cleaning up here.. ok.
If the gen_fsm is not part of a supervision tree, a stop function
may be useful. The gen_fsm will automatically call the callback
function terminate(Reason, StateName, StateData)
if another
of the callback functions returns {stop, Reason, State2}
instead of {next_state,...}
. Example:
-module(code_lock). ... -export([stop/0]). ... stop() -> gen_fsm:send_all_state_event(code_lock, stop). ... handle_event(stop, _StateName, StateData) -> {stop, normal, StateData}. ... terminate(normal, _StateName, _StateData) -> ok.
If the gen_fsm should be able to receive other messages than
events, the callback function
handle_info(Info, StateName, StateData)
must be implemented
to handle them. Examples on other messages could be exit messages,
if the gen_fsm is linked to other processes (than the supervisor)
and trapping exit signals.
handle_info({'EXIT', Pid, Reason}, StateName, StateData) -> ..code to handle exits here.. {next_state, StateName1, StateData1}.