- # 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
- 1. `next_state_sync/3` A pure fun that is caled with `[Event, PrevStateName, StateData]`
- and returns just a `StateName`, or `{stop, ...}`
- 1. `next_state_async/3` A pure fun that is caled with `[Event, PrevStateName, StateData]`
- 1. `after_state/2` (optional) A `StateData` modifying fun called with `[LeavingStateName, StateData]`
- 1. `before_state/2` (optional) A `StateData` modifying fun called with `[EnteringStateName, StateData]`
- 1. `on_event/2` (optional) A `StateData` modifying fun called with `[EventName, StateData]`
- 1. `state_change_sync/4` The most like normal FSM state callback, `[Event, PrevStateName, NextState, StateData]`
- 1. `state_change_async/5` ... ^, plus the `From` argument
- 1. `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
- 1. `after_state`
- 1. `on_event`
- 1. `before_state`
- 1. `state_change_[sync, async]`
- ### What it looks like
- ```erlang
- -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}.
- ```