[erlang-questions] Implementing a loop that quits on an external event.

Iñaki Garay igarai@REDACTED
Sun Dec 18 06:12:57 CET 2011


On Fri, Dec 16, 2011 at 19:40, Iñaki Garay <igarai@REDACTED> wrote:
> Hello everyone,
>
> Short story:
> how could I implement a loop that can quit on an external event using
> the OTP behaviours?
> One option I can think of is a gen_fsm that has a transition to the
> current state, and sends itself a message at the end of the
> transition.
> Another is having two processes, one watching for the external event
> or message, and when it occurs sets some sort of shared variable, e.g.
> in an ETS table, while another executes the loop and checks whether
> the shared variable was set to 'quit'.
> Any other options?
> Problems with these?
> Existing examples of this situation?
> how is this usually done?
>
> Long story:
> You have a dream^W game, a small multi-agent simulator.
> You have an environment, which is the state of the world.
> Agents are gen_servers, you give them a percept and ask for an action.
> Actions modify the world.
> During  one iteration of the simulation loop, it tells each agent what
> it perceives and asks what action it will perform, and then applies
> the action's effects on the environment state.
>
> The environment simulator is a gen_fsm.
> It has (among others), two states: paused, and running.
>
> When paused, if it receives a 'run' message, it transitions to the
> running state.
> If it receives a 'quit' message, it stops.
>
> When running, if it receives a 'pause' message, it switches to the paused state.
> Also when running, it "runs the simulation".
>
> Now, the 'running' state shouldn't have to wait each iteration for
> input to continue on to the next simulation iteration.
> I can think of two ways to do this:
>
> 1. An 'epsilon transition', that is, a transition that fires without
> input to the FSM.
> The simulator gen_fsm could send_event 'runtime' to itself, at the end of
> each iteration.
> Every time it receives a 'run' message, one ittimeeration of the
> simulation cycle is executed.
>
> 2. Outsource the main loop to another process.
> The gen_fsm in the running state now waits for the 'pause' message,
> and when received, signals the other process running the tight loop.
> The 'signal' could be a message: the looping process has a receive
> with a timeout of 0, every time it checks if there is no message it
> continues the cycle.
> Or it could be some shared state, e.g. something in an ETS table,
>
> In the first case, I can imagine the gen_fsm executing an iteration,
> and simulatenously the pause event is received.
> At the end, it sends itself the 'run' message, and transitions to the
> 'paused' state.
> A spurious message is left in the queue, possibly messing things up later on.
> Also, waiting for a message, even one you "just sent", may possibly
> take longer than expected than just comparing a value, or even
> executing a receive statement with a timeout of 0.
>
> In the second case, shared state, and a more complicated design.
>


> Am I overthinking?
Yes I was.

>
> many thanks in advanced,
> Iñaki Garay.

I found a nice (and I suppose) correct way, and will reply to my own
email for posterity. I'm terribly vexed when I find those forums posts
or emails marked as [SOLVED] and the answer is not explained. :)

The solution, as usual, is to RTFM.
The documentation for gen_fsm module states in the callback functions
section that the function Module:StateName(Event, StateData) should
return Result, where

Result = {next_state,NextStateName,NewStateData}
  | {next_state,NextStateName,NewStateData,Timeout}
  | {next_state,NextStateName,NewStateData,hibernate}
  | {stop,Reason,NewStateData}

If a Timeout is specified in the result, the FSM will transition to
NextStateName, and after Timeout the FSM will call the function with
the same name as the NextStateName.
So to implement a loop in the FSM, i.e. a self-transition to the
current state after no time, one can do:

current_state(timeout, State) -> {next_state, current_state, State, 0};

To break out of the loop, simple use another transition:

current_state(break, State) -> {next_state, another_state, State}.

Simple and elegant :)
I can't see how the problems I thought of earlier apply to this.
One gotcha is that one must remember to add the timeout to any other
transition that moves to the looping state.

Following is a small example which can be run in the shell, hopefully
helpful to other beginners.

%% fsm.erl :begin code example
% The fsm state will be a single number indicating how many
transitions have been executed, starting at 0.
% Two states, running and paused.

-module(fsm).

-compile([export_all]).

start() ->
    gen_fsm:start({local,?MODULE}, ?MODULE, 0, []).

% type less in the shell.
run() ->
    gen_fsm:send_event(?MODULE, run).
pause() ->
    gen_fsm:send_event(?MODULE, pause).
quit() ->
    gen_fsm:send_event(?MODULE, quit).

% The FSM starts in the paused state.
init(0) ->
    {ok, paused, 0}.

terminate(normal, _StateName, _StateData) ->
    ok.

% Each transition will print the current state, the input received,
and the state.
% The timeout is set to 1000 to give us time to type in the shell
without the FSM transitioning hundreds of thousands of times.
paused(quit, State) ->
    io:format("paused  : quit    : ~p~n", [State]),
    {stop, normal, State};
% We must remember to add the timeout to any other transition that
moves to the looping state.
paused(run, State) ->
    io:format("paused  : run     : ~p~n", [State]),
    {next_state, running, State + 1, 1000}.

running(pause, State) ->
    io:format("running : pause   : ~p~n", [State]),
    {next_state, paused, State + 1};
running(timeout, State) ->
    io:format("running : timeout : ~p~n", [State]),
    {next_state, running, State + 1, 1000}.
%% end code example



%% begin shell example
Erlang R14B04 (erts-5.8.5) [source] [smp:2:2] [rq:2] [async-threads:0]
[hipe] [kernel-poll:false]

Eshell V5.8.5  (abort with ^G)
1> fsm:start().
{ok,<0.34.0>}
2> fsm:run().
paused  : run     : 0
ok
running : timeout : 1
running : timeout : 2
running : timeout : 3
running : timeout : 4
3> fsm:pause().
running : pause   : 5
ok
4> fsm:run().
paused  : run     : 6
ok
running : timeout : 7
running : timeout : 8
running : timeout : 9
5> fsm:pause().
running : pause   : 10
ok
6> fsm:quit().
paused  : quit    : 11
ok
%% end shell example


criticism & guidance welcome,
Iñaki Garay.



More information about the erlang-questions mailing list