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/1Just like normalgen_fsminitnext_state_sync/3A pure fun that is caled with[Event, PrevStateName, StateData]and returns just aStateName, or{stop, ...}next_state_async/3A pure fun that is caled with[Event, PrevStateName, StateData]after_state/2(optional) AStateDatamodifying fun called with[LeavingStateName, StateData]before_state/2(optional) AStateDatamodifying fun called with[EnteringStateName, StateData]on_event/2(optional) AStateDatamodifying fun called with[EventName, StateData]state_change_sync/4The most like normal FSM state callback,[Event, PrevStateName, NextState, StateData]state_change_async/5... ^, plus theFromargumenthandle_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_stateon_eventbefore_statestate_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