A Quick look at OTP's Gen_Fsm Behaviour

Overview

Erlang's Behaviours are formalizations of common patterns where the is divide into a pre-canned behavior module and a callback module that has to be implemented. OTP offers gen_fsm which used for implementing Finite State Machines. This article will explore the OTP gen_fsm and use it to implement a relatively simple but real FSM. In doing so well will not only learn gen_fsm but more importantly get a good idea of the limitations of FSM. Then I will try and re-implement the same app using a Statechart. This will not only show you the major benefits of using s Stateschart but also introduce an new Statechart behavior.

gen_fsm

gen_fsm implements the Mearly FSM model, where given that you are in state S when you receive and event E you perform action A that changes your state to S'. In fsm_gen these transition rules are coded by callbacks for each state/event pair. This call back is effectively the action A and returns the next state. The callback also takes and returns some data, a message if you like, possibly associated with the event. The callback can ofcourse stop the FSM by returning a 'stop' instead of the usual 'next_state'.

One of the very nice things about fsm_gen is its supports the idea of states timing-out, and the idea of All State Events, and has the ability to hibernate, .

Module:StateName(Event, StateData) -> Result

Types:

Event = timeout | term()
StateData = term()
Result = {next_state,NextStateName,NewStateData}
  | {next_state,NextStateName,NewStateData,Timeout}
  | {next_state,NextStateName,NewStateData,hibernate}
  | {stop,Reason,NewStateData}
 NextStateName = atom()
 NewStateData = term()
 Timeout = int()>0 | infinity
 Reason = term()

fsm_gen module offers the following methods and expects the developer to implement the following their respective callbacks:

gen_fsm export Callback
gen_fsm:start_link Module:init/1
gen_fsm:send_event Module:StateName/2
gen_fsm:send_all_state_event Module:handle_event/3
gen_fsm:sync_send_event Module:StateName/3
gen_fsm:sync_send_all_state_even Module:handle_sync_event/4
gen_fsm:send_event_after Module:StateName/2
gen_fsm:start_timer Module:StateName/2

There are also the usual:

Module:handle_info/3

Module:terminate/3

Module:code_change/4

A Simple Example: A CD Player

The Interface

User Interface Object 1 - drawer closed 2 - drawer open 3 - drawer closing 4 - cd stopped 5 - CD playing 6 - cd paused
Play disabled disabled disabled enabled disabled enabled
Pause disabled disabled disabled disabled enabled enabled
Stop disabled disabled disabled disabled enabled enabled
Prev track disabled disabled disabled enabled enabled enabled
Rev disabled disabled disabled enabled enabled enabled
Fwd disabled disabled disabled enabled enabled enabled
Nxt Track disabled disabled disabled enabled enabled enabled
Eject enabled enabled disabled enabled enabled enabled
Load disabled enabled disabled disabled disabled disabled

First let us code the bare minimum:

-module(cdplayer).
-behaviour(gen_fsm).

-export([on/0, off/0, terminate/3]).
-export([handle_event/3]).
-export([init/1]).



%% Manually start the cd player. 
%% This fires init/1 with an empty list of data, in case no current cd
on() ->
    gen_fsm:start({local, cdplayer}, cdplayer, [], []).

%% sends a stop event that will be always handled by the handle_event callback, 
%% regardless of the current state.
off() ->
    gen_fsm:send_all_state_event(cdplayer, stop).



%% Initializes the FSM to start in the drawer_closed state.
init(CurrentCD) ->
    {ok, drawer_closed, CurrentCD}.

%% Tells the FSM to stop, giving a normal reason. This will
%% fire the   terminate/3 callback.
handle_event(stop, _StateName, StateData) ->
    {stop, normal, StateData}.


terminate(normal, _StateName, _StateData) ->
    ok.

%% The following are implemented to meet the contract of the gen_fsm 
%% behaviour and to stop the compiler from complaining.

handle_info(Info, _StateName, _StateData) ->
    io:format("handle_info: ~w, ~w, ~w ~n", [Info, _StateName, _StateData]),
	ok.

handle_sync_event(Event, From, StateName, StateData) ->
    io:format("handle_info: ~w, ~w, ~w, ~w ~n", [Event, From, StateName, StateData]),
	ok.

code_change(_OldVsn, StateName, StateData, _Extra) ->
	{ok, StateName, StateData}.

Running this is as easy as:

Eshell V5.6.1  (abort with ^G)
1> c(cdplayer).
{ok,cdplayer}
2> cdplayer:on().
{ok,<0.37.0>}
3> cdplayer:off().
ok
4> 


The Spec

Current State Event and Conditions Actions Next State
init Application Started close_drawer 1 - drawer closed
1 - drawer_closed Eject Button Clicked open_drawer 2 - drawer_open
2 - drawer open Eject Button Clicked close_drawer 3 - drawer closing
2 - drawer open CD Loaded none 2 - drawer open
3 - drawer closing no cd inside none 1 - drawer closed
3 - drawer closing cd inside none 4 - cd stopped
4 - cd stopped Eject button clicked open_drawer 2 - drawer open
4 - cd stopped Play button clicked play 5 - CD playing
5 - CD playing Eject button clicked stop , open drawer 2 - drawer open
5 - CD playing Stop button clicked stop 4 - cd stopped
5 - CD playing Pause button clicked pause 6 - cd paused
6 - cd paused Eject button clicked open_drawer 2 - drawer open
6 - cd paused Stop button clicked stop 4 - cd stopped
6 - cd paused Pause button clicked play 5 - CD playing
6 - cd paused Pause button clicked play 5 - CD playing
-module(vcr).
-behaviour(gen_fsm).

-export([start_link/1]).
-export([play/0, pause/0, stop/0, prev/0, nxt/0, rev/0, fwd/0, eject/0]).
-export([init/1, locked/2, open/2]).
.
.
.
init(CurrentCd) ->
    {ok, drawer_closed, CurrentCd}.

drawer_closed(eject_clicked, CurrentCd) -> 
    open(), {next_state, drawer_open, CurrentCd}.

drawer_open(eject_clicked, CurrentCd) -> 
    close(), {next_state, drawer_closing, CurrentCd}.

drawer_open({cd_loaded, Cd}, CurrentCd) -> 
    close(), {next_state, drawer_open, Cd}.

drawer_closing(eject_clicked, CurrentCd) -> 
    close(), {next_state, drawer_closing, CurrentCd}.

Current State Event and Conditions Actions Next State
init Application Started close_drawer 1 - drawer closed
1 - drawer_closed Eject Button Clicked open_drawer 2 - drawer_open
2 - drawer_open Eject Button Clicked close_drawer 3 - drawer closing
3 - drawer closing no cd inside none 1 - drawer closed
3 - drawer closing cd inside none 4 - cd stopped
4 - cd stopped Eject button clicked open_drawer 2 - drawer open
4 - cd stopped Play button clicked play 5 - CD playing
5 - CD playing Eject button clicked stop , open drawer 2 - drawer open
5 - CD playing Stop button clicked stop 4 - cd stopped
5 - CD playing Pause button clicked pause 6 - cd paused
6 - cd paused Eject button clicked open_drawer 2 - drawer open
6 - cd paused Stop button clicked stop 4 - cd stopped
6 - cd paused Pause button clicked play 5 - CD playing
6 - cd paused Pause button clicked play 5 - CD playing