Skip to content

Instantly share code, notes, and snippets.

@reiddraper
Created August 17, 2012 19:20
Show Gist options
  • Save reiddraper/3381753 to your computer and use it in GitHub Desktop.
Save reiddraper/3381753 to your computer and use it in GitHub Desktop.
FSM Strawman

FSM Strawman

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.

Motivation

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.

Current Idea

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:

  1. init/1 Just like normal gen_fsm init
  2. next_state_sync/3 A pure fun that is caled with [Event, PrevStateName, StateData] and returns just a StateName, or {stop, ...}
  3. next_state_async/3 A pure fun that is caled with [Event, PrevStateName, StateData]
  4. after_state/2 (optional) A StateData modifying fun called with [LeavingStateName, StateData]
  5. before_state/2 (optional) A StateData modifying fun called with [EnteringStateName, StateData]
  6. on_event/2 (optional) A StateData modifying fun called with [EventName, StateData]
  7. state_change_sync/4 The most like normal FSM state callback, [Event, PrevStateName, NextState, StateData]
  8. state_change_async/5 ... ^, plus the From argument
  9. handle_info/3 (optional)

The order of callbacks is defined to be:

  1. next_state_[sync, async] call this first just to get the name of the next state, as it doesn't modify state data at all
  2. after_state
  3. on_event
  4. before_state
  5. state_change_[sync, async]

What it looks like

-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}.
@slfritchie
Copy link

Perhaps relevant? Garrett Smith's OTP overhaul of by-convention framework, https://github.com/gar1t/e2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment