I've been thinking about some of the shortcomings of gen_fsm
, and some
common FSM actions I've either implemented myself, or seen my FSMs suffer
from not having. I've been playing around with a couple ideas for alternative
ways to describe an FSM that would still be driven by gen_fsm
. This is the
the first concrete idea I've come up with, but it certainly still is far from
completely fleshed out. That said, I think there's enough to start getting
feedback.
I've often noticed certain patterns in mine and others' FSMs. Things like,
update the StateData
in this way whenever this event is received, regardless
of whate state-name I'm currently in. Or count how many times I leave
the any state and go into the "overflow" state, even though there may
be several places in code this happens.
What I've come up with so far is a set of callbacks that you (optionally)
implement. I say optionally because several of them have sane defaults.
The options are then passed in as a proplist
as args to the "FSM translator",
a module that implements gen_fsm
, but goes through state changes just like
your spec says. The functions you can implement are:
init/1
Just like normalgen_fsm
initnext_state_sync/3
A pure fun that is caled with[Event, PrevStateName, StateData]
and returns just aStateName
, or{stop, ...}
next_state_async/3
A pure fun that is caled with[Event, PrevStateName, StateData]
after_state/2
(optional) AStateData
modifying fun called with[LeavingStateName, StateData]
before_state/2
(optional) AStateData
modifying fun called with[EnteringStateName, StateData]
on_event/2
(optional) AStateData
modifying fun called with[EventName, StateData]
state_change_sync/4
The most like normal FSM state callback,[Event, PrevStateName, NextState, StateData]
state_change_async/5
... ^, plus theFrom
argumenthandle_info/3
(optional)
The order of callbacks is defined to be:
next_state_[sync, async]
call this first just to get the name of the next state, as it doesn't modify state data at allafter_state
on_event
before_state
state_change_[sync, async]
-module(simple_fsm).
-export([start_link/1]).
-record(state, {count :: integer(),
max_count :: integer()}).
start_link(Args) ->
%% fsm2 is the module that takes your spec
%% and runs an FSM according to it
fsm2:start_link([spec(), Args]).
spec() ->
[{init, fun init/1},
{on_event, fun on_event/2},
{next_state_sync, fun next_state_sync/3},
{next_state_async, fun next_state_async/3},
{before_state, fun before_state/2},
{after_state, fun after_state/2},
{state_change_sync, fun state_change_sync/4},
{state_change_async, fun state_change_async/5}].
init([StartingCount, MaxCount]) ->
#state{count=StartingCount,
max_count=MaxCount}.
on_event({incr, _Amount}, State=#state{count=C,
max_count=C}) ->
State;
on_event({incr, Amount}, State=#state{count=C,
max_count=M}) when (C + Amount) =< M ->
State#state{count=C + Amount};
on_event({incr, _Amount}, State=#state{max_count=M}) ->
State#state{count=M};
on_event({decr, Amount}, State=#state{count=C}) ->
Count = erlang:max(0, (C - Amount)),
State#state{count=Count};
%% catchall, w/ identity fun
on_event(_Event, State) ->
State.
after_state(full, State) ->
lager:debug("leaving full state"),
State;
after_state(_StateName, State) ->
State.
before_state(full, State) ->
lager:debug("entering full state"),
State;
before_state(_StateName, State) ->
State.
next_state_sync(_Event, _PrevStateName, State=#state{count=C, max_count=C}) ->
{next_state, max, State};
next_state_sync(_Event, _PrevStateName, State) ->
{next_state, free, State}.
next_state_async(_Event, _PrevStateName, State) ->
{stop, unexpected, State}.
state_change_sync(status, _PrevState, full, State=#state{count=C}) ->
Reply = io_lib:format("in full state with count ~p", [C]),
{reply, Reply, State};
state_change_sync(status, _PrevState, free, State=#state{count=C}) ->
Reply = io_lib:format("in free state with count ~p", [C]),
{reply, Reply, State}.
state_change_async(_Event, _PrevState, _CurrentState, StateData, _From) ->
{stop, unexpected, StateData}.
Perhaps relevant? Garrett Smith's OTP overhaul of by-convention framework, https://github.com/gar1t/e2