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 |
