Consider I have a FSM implemented with gen_fsm. For a some Event in a some StateName I should write data to database and reply to user the result. So the following StateName is represented by a function:
statename(Event, _From, StateData) when Event=save_data->
case my_db_module:write(StateData#state.data) of
ok -> {stop, normal, ok, StateData};
_ -> {reply, database_error, statename, StateData)
end.
where my_db_module:write is a part of non-functional code implementing actual database write.
I see two major problems with this code: the first, a pure functional concept of FSM is mixed by part of non-functional code, this also makes unit testing of FSM impossible. Second, a module implementing a FSM have dependency on particular implementation of my_db_module.
In my opinion, two solutions are possible:
Implement my_db_module:write_async as sending an asynchronous message to some process handling database, do not reply in statename, save From in StateData, switch to wait_for_db_answer and wait result from db management process as a message in a handle_info.
statename(Event, From, StateData) when Event=save_data-> my_db_module:write_async(StateData#state.data), NewStateData=StateData#state{from=From}, {next_state,wait_for_db_answer,NewStateData} handle_info({db, Result}, wait_for_db_answer, StateData) -> case Result of ok -> gen_fsm:reply(State#state.from, ok), {stop, normal, ok, State}; _ -> gen_fsm:reply(State#state.from, database_error), {reply, database_error, statename, StateData) end.
Advantages of such implementation is possibility to send arbitrary messages from eunit modules without touching actual database. The solution suffers from possible race conditions, if db reply earlier, that FSM changes state or another process send save_data to FSM.
Use a callback function, written during init/1 in StateData:
init([Callback]) -> {ok, statename, #state{callback=Callback}}. statename(Event, _From, StateData) when Event=save_data-> case StateData#state.callback(StateData#state.data) of ok -> {stop, normal, ok, StateData}; _ -> {reply, database_error, statename, StateData) end.
This solution doesn't suffer from race conditions, but if FSM uses many callbacks it really overwhelms the code. Although changing to actual function callback makes unit testing possible it doesn't solves the problem of functional code separation.
I am not comfortable with all of this solutions. Is there some recipe to handle this problem in a pure OTP/Erlang way? Of may be it is my problem of understating principles of OTP and eunit.