Big state machines

Vance Shipley vances@REDACTED
Fri Apr 22 18:31:12 CEST 2005


Ulf,

Having read your paper "Programming Models for Concurrency"(*)
I think you have won me over to the pure erlang side.  I have
some FSMs which require SDL saves and are pretty ugly in gen_fsm.
Having selective receives will really simplify things for me.
However I'm not convinced that plain_fsm is solving the problem
the way I want it solved.

In your plain_fsm documentation you say:

   "... figure out where you really want to handle system messages. 
    Normally, it will suffice to do it in a fairly stable state."

In one of your examples you didn't handle system messages while 
in the transient states waiting for a reply.  Is that a good idea?

This suggests that the coder should have control over how system
messages are processed.  Given that I want to do my own receiving,
so that I can do so selectively, it seems to me that I want to
manage how the system events appear in the receives.

So I wrote a behaviour module which uses some of the gen_fsm API
but takes out the main loop.  I built macros to make the handling
of system messages a little less verbose so that the real code is 
more of a focus.  I think I quite like this.

The new sdl_fsm looks like this:

-module(sdl_fsm).
-export([init/1, terminate/4, code_change/4]).
-export([s/2, y/2, x/2]).

-include("sys_fsm.hrl").

-record(state, {}).

init(_Args) ->
	process_flag(trap_exit, true),
	{ok, s, #state{}}.

%%         ___
%%        (_s_)
%%          |
%%       +--+--+
%%     __|_  __|_
%%     >_a_| >_b_|
%%      _|_   _|_
%%     (_x_) (_y_)
%%
s(SysData, StateData) ->
	receive
		?SYSTEM(s, SysData, StateData);
		?EXIT(s, SysData, StateData);
		a ->
			?DEBUG(a, s, SysData, StateData),
			x(SysData, StateData);
		b ->
			?DEBUG(b, s, SysData, StateData),
			y(SysData, StateData);
		Msg ->
			?DEBUG(Msg, s, SysData, StateData),
			s(SysData, StateData)
	end.

%%            ___
%%           (_x_)
%%             |
%%       +-----+------+
%%     __|_  __|_   __|_
%%     >_a_| >_b_| /_*_/
%%      _|_   _|_
%%     (_x_) (_y_)
%%
x(SysData, StateData) ->
	receive
		?SYSTEM(x, SysData, StateData);
		?EXIT(x, SysData, StateData);
		a ->
			?DEBUG(a, x, SysData, StateData),
			x(SysData, StateData);
		b ->
			?DEBUG(b, x, SysData, StateData),
			y(SysData, StateData)
	end.

%%            ___
%%           (_y_)
%%             |
%%       +-----+------+-----+
%%     __|_  __|_   __|_  __|_
%%     >_a_| >_b_| /_c_/ /_d_/
%%      _|_   _|_
%%     (_x_) (_y_)
%%
y(SysData, StateData) ->
	receive
		?SYSTEM(y, SysData, StateData);
		?EXIT(y, SysData, StateData);
		a ->
			?DEBUG(a, y, SysData, StateData),
			x(SysData, StateData);
		b ->
			?DEBUG(b, y, SysData, StateData),
			y(SysData, StateData);
		Msg when Msg /= c, Msg /= d ->
			?DEBUG(Msg, y, SysData, StateData),
			y(SysData, StateData)
	end.


%%
%% sys_fsm callbacks
%%
terminate(_Reason, _StateName, _SysData, _StateData) -> ok.

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


-------------- next part --------------

-record(sys, {parent, name, debug}).

-define(SYSTEM(StateName, SysData, StateData),
		{system, From, Msg} ->
			sys:handle_system_msg(Msg, From, SysData#sys.parent,
					sys_fsm, SysData#sys.debug,
					[?MODULE, StateName, SysData, StateData])).

-define(EXIT(StateName, SysData, StateData),
		{'EXIT', Parent, Reason} = Msg when Parent == SysData#sys.parent ->
			gen_fsm:terminate(Reason, SysData#sys.name, Msg, ?MODULE,
					StateName, StateData, SysData#sys.debug)).

-define(DEBUG(Msg, StateName, SysData, StateData),
		case SysData#sys.debug of
			[] -> ok;
			Debug ->
				sys:handle_debug(Debug,
						fun(Device, Event, Extra) ->
							gen_fsm:print_event(Device, Event, Extra)
						end, {SysData#sys.name, StateName}, {in, Msg})
		end).

-------------- next part --------------
%%% sys_fsm.erl
%%%
%%% Behaviour module for an FSM process which receives messages 
%%% directly.  Much of this module is ruthlessly culled from gen_fsm.
%%%
-module(sys_fsm).

%% API
-export([start/3, start_link/3, start/4, start_link/4, format_status/2]).
-export([behaviour_info/1]).
%% internal
-export([init_it/6]).
%% sys callbacks
-export([system_continue/3, system_terminate/4, system_code_change/4]).

-include("sys_fsm.hrl").


behaviour_info(callbacks) ->
    [{init,1}, {terminate,3},{code_change,4}];
behaviour_info(_Other) ->
    undefined.

%% the start functions have the same syntax as gen_fsm
start(Mod, Args, Options) ->
	gen:start(?MODULE, nolink, Mod, Args, Options).
start(Name, Mod, Args, Options) ->
	gen:start(?MODULE, nolink, Name, Mod, Args, Options).
start_link(Mod, Args, Options) ->
	gen:start(?MODULE, link, Mod, Args, Options).
start_link(Name, Mod, Args, Options) ->
	gen:start(?MODULE, link, Name, Mod, Args, Options).

%% the user's init/1 callback has the same results as 
%% gen_fsm's but without the timeout option.
init_it(Starter, self, Name, Mod, Args, Options) ->
	init_it(Starter, self(), Name, Mod, Args, Options);
init_it(Starter, Parent, Name, Mod, Args, Options) ->
	Debug = gen:debug_options(Options),
	SysData = #sys{parent = Parent, name = Name, debug = Debug},
	case catch Mod:init(Args) of
		{ok, StateName, StateData} ->
			proc_lib:init_ack(Starter, {ok, self()}), 	    
			Mod:StateName(SysData, StateData);
		{stop, Reason} ->
			proc_lib:init_ack(Starter, {error, Reason}),
			exit(Reason);
		ignore ->
			proc_lib:init_ack(Starter, ignore),
			exit(normal);
		{'EXIT', Reason} ->
			proc_lib:init_ack(Starter, {error, Reason}),
			exit(Reason);
		Else ->
			Error = {bad_return_value, Else},
			proc_lib:init_ack(Starter, {error, Error}),
			exit(Error)
		end.

%%
%% callbacks for system messages
%%
system_continue(_Parent, Debug, [Module, StateName, SysData, StateData]) ->
	Module:StateName(SysData#sys{debug = Debug}, StateData).

system_terminate(Reason, _Parent, Debug,
		[Module, StateName, SysData, StateData]) ->
	gen_fsm:terminate(Reason, SysData#sys.name, [], Module,
			StateName, StateData, Debug).

system_code_change([Module, StateName, SysData, StateData],
		Module, OldVsn, Extra) ->
	case catch Module:code_change(OldVsn, StateName, StateData, Extra) of
		{ok, NewStateName, NewStateData} ->
			{ok, [Module, NewStateName, SysData, NewStateData]};
		Else -> Else
	end.

format_status(Opt, StatusData) ->
	[PDict, SysState, Parent, Debug, Misc] = StatusData,
	[Module, StateName, SysData, StateData] = Misc,
	GenFsmMisc = [SysData#sys.name, StateName, StateData, Module, 0],
	gen_fsm:format_status(Opt, [PDict, SysState, Parent, Debug, GenFsmMisc]).



More information about the erlang-questions mailing list