Erlang Central

Difference between revisions of "Cascading Behaviours"

From ErlangCentral Wiki

Line 1: Line 1:
==1. Introduction==
+
=1. Introduction=
  
 
===Behaviours===  
 
===Behaviours===  

Revision as of 11:21, 29 June 2006

Contents

1. Introduction

Behaviours

Behaviour modules encapsulate some certain functionality for reuse by other modules. The definition of a behaviour includes not only it's exported functions but a list of functions which a callback module must implement. OTP includes several behaviour modules with the most common being gen_server and gen_fsm.

2. Using gen_fsm

Behaviour

To declare your module as a behaviour callback module you include the -behaviour attribute.

Code listing 2.1: Behaviour Attribute

-behaviour(gen_fsm).

Note: All this really does is to enable compilation time checking that you have exported the required callbacks.

Exports

A module which uses the gen_fsm behaviour must export the callbacks which the gen_fsm module calls to handle events. These include handlers for common gen_fsm events as well as the handlers for your user defined states.

Code listing 2.2: Required gen_fsm Callbacks

-export([init/1, handle_event/3, handle_sync_event/4,
         handle_info/3, terminate/3, code_change/4]).

Code listing 2.3: State Handler Callbacks

-export([idle/2, busy/2]).


Startup

To start a process implemented in a module as a gen_fsm behaviour you may call gen_fsm:start/3. In response this function will call the init/1 callback in your module. After your init/1 function returns a result gen_fsm:start_link/3 will return the appropriate result to the calling process. Normally the result is to create a new process where the gen_fsm module is running it's internal main loop function waiting for messages to arrive.

Code listing 2.4: Starting a Finite State Machine

2> <span class="input">{ok, FSM} = gen_fsm:start(simple_fsm, [], []).</span>
{ok,<0.36.0>}

Events

To send an event to your finite state machine process you may call gen_fsm:send_event/2.

Code listing 2.5: Sending an Event

2> <span class="input">gen_fsm:send_event(FSM, foo).</span>

ok

The result is that a message is sent to your FSM process tagged so that the gen_fsm module may recognize it as a gen_fsm generated event. When the main loop function in gen_fsm receives this message it will call the state handler callback in your module corresponding to the current state the FSM process is in.

3. An Example FSM

The Module

Below is a very simple example of a module implemented with a gen_fsm behaviour. In this module we will implement just one state and the only thing it will really do is to be chatty about what is going on.

Code listing 3.1: A Chatty gen_fsm Callback Module

-module(chatty_fsm).

-export([init/1, handle_event/3, handle_sync_event/4,
         handle_info/3, terminate/3, code_change/4]).
-export([idle/2]).

-behaviour(gen_fsm).

-record(state, {}).

init(Args) ->
    process_flag(trap_exit, true),
    io:fwrite("gen_fsm called ~w:init(~w)~n", [?MODULE, Args]),
    {ok, idle, #state{}}.

idle(Event, StateData) ->
    io:fwrite("gen_fsm called ~w:idle(~w, ~w)~n", 
        [?MODULE, Event, StateData]),
    {next_state, idle, StateData}.

handle_event(Event, StateName, StateData) ->
    io:fwrite("gen_fsm called ~w:handle_event(~w, ~w, ~w)~n",
        [?MODULE, Event, StateName, StateData]),
    {next_state, StateName, StateData}.

handle_sync_event(Event, From, StateName, StateData) ->
    io:fwrite("gen_fsm called ~w:handle_sync_event(~w, ~w, ~w, ~w)~n",
        [?MODULE, Event, From, StateName, StateData]),
    {next_state, StateName, StateData}.

handle_info(Info, StateName, StateData) ->
    io:fwrite("gen_fsm called ~w:handle_info(~w, ~w, ~w)~n",
        [?MODULE, Info, StateName, StateData]),
    {next_state, StateName, StateData}.

terminate(Reason, StateName, StateData) ->

    io:fwrite("gen_fsm called ~w:terminate(~w, ~w, ~w)~n",
        [?MODULE, Reason, StateName, StateData]).

code_change(OldVsn, StateName, StateData, Extra) ->
    io:fwrite("gen_fsm called ~w:code_change(~w, ~w, ~w, ~w)~n",
        [?MODULE, OldVsn, StateName, StateData, Extra]),
    {ok, StateName, StateData}.

Running the Chatty FSM

When we start this module and send it an event as above we will see the functions in our callback module being called.

Code listing 3.2: Starting the Chatty FSM

1> <span class="input">{ok, FSM} = gen_fsm:start(chatty_fsm, [], []).</span>
gen_fsm called chatty_fsm:init([])
{ok,<0.31.0>}

Code listing 3.3: Sending the Chatty FSM an Event

2> <span class="input">gen_fsm:send_event(FSM, foo).</span>

gen_fsm called chatty_fsm:idle(foo, {state})
ok

4. Creating a New Behaviour

A Chatty FSM Behaviour

Now we may decide that a chatty FSM is a useful thing in a very general way. If we want to make other FSMs chatty we can put this functionality into a behaviour which our FSM modules may behave to. The basic idea is that we will implement the callbacks which the gen_fsm module will call as pass through functions to the callbacks in the user's module. These pass through functions will simply perform the chattyness and then call the FSM module's identical callbacks.

The Module

Note:

It is not suggested that this module performs a useful purpose as it is. There are better solutions to add event tracing including passing the gen_fsm start function the {debug, [trace]} option. The intent is to demonstrate an enhanced behaviour.

Code listing 4.1: A Chatty FSM Behaviour Module

-module(chatty_fsm).

%% <span class="comment">Export the same API as gen_fsm.</span>
-export([start/3, start/4, start_link/3, start_link/4,
         send_event/2, sync_send_event/2, sync_send_event/3,
         send_all_state_event/2, sync_send_all_state_event/2,
         sync_send_all_state_event/3, reply/2, start_timer/2,
         send_event_after/2,cancel_timer/1]).

%% <span class="comment">Export the callbacks gen_fsm expects.</span>
-export([init/1, handle_event/3, handle_sync_event/4,
         handle_info/3, terminate/3, code_change/4]).
-export([state/2]).

%% <span class="comment">Define the behaviour's required callbacks.</span>
-export([behaviour_info/1]).

behaviour_info(callbacks) ->

    [{init,1},{handle_event,3},{handle_sync_event,4},
        {handle_info,3}, {terminate,3},{code_change,4}];
behaviour_info(_Other) ->
    undefined.

%% <span class="comment">Define this module as a gen_fsm callback module.</span>
-behaviour(gen_fsm).

%% <span class="comment">State data record.</span>
-record(state, {module, state, data}).

%% <span class="comment">Users will use these start functions instead of gen_fsm's.</span>
%% <span class="comment">We add the user's module name to the arguments and call</span>
%% <span class="comment">gen_fsm's start function with our module name instead.</span>

start(Mod, Args, Options) ->
    gen_fsm:start(?MODULE, [Mod, Args], Options).

start(Name, Mod, Args, Options) ->
    gen_fsm:start(Name, ?MODULE, [Mod, Args], Options).

start_link(Mod, Args, Options) ->
    gen_fsm:start_link(?MODULE, [Mod, Args], Options).

start_link(Name, Mod, Args, Options) ->
    gen_fsm:start_link(Name, ?MODULE, [Mod, Args], Options).

%% <span class="comment">These functions are just pass through to gen_fsm.</span>
%% <span class="comment">They are included for completeness only.</span>

send_event(Name, Event) ->
    gen_fsm:send_event(Name, Event).

sync_send_event(Name, Event) ->
    gen_fsm:sync_send_event(Name, Event).

sync_send_event(Name, Event, Timeout) ->
    gen_fsm:sync_send_event(Name, Event, Timeout).

send_all_state_event(Name, Event) ->
    gen_fsm:send_all_state_event(Name, Event).

sync_send_all_state_event(Name, Event) ->
    gen_fsm:sync_send_all_state_event(Name, Event).

sync_send_all_state_event(Name, Event, Timeout) ->

    gen_fsm:sync_send_all_state_event(Name, Event, Timeout).

start_timer(Time, Msg) ->
    gen_fsm:start_timer(Time, Msg).

send_event_after(Time, Event) ->
    gen_fsm:send_event_after(Time, Event).

cancel_timer(Ref) ->
    gen_fsm:cancel_timer(Ref).

reply(Caller, Reply) ->
    gen_fsm:reply(Caller, Reply).

%% <span class="comment">Our start function above added the user's module name</span>
%% <span class="comment">to the arguments.  We store this in our state data record.</span>

%% <span class="comment">After performing our chattyness we run the user's init/1</span>
%% <span class="comment">and store the user's next state name and state data in</span>
%% <span class="comment">our internal state data record for later reference.</span>
init([Mod, Args]) ->
    io:fwrite("~w:init(~w) -> ", [Mod, Args]),
    case Mod:init(Args) of
        {ok, ExtStateName, ExtStateData} -> 
            io:fwrite("    {ok, ~w, ~w}~n", [ExtStateName, ExtStateData]),
            StateData = #state{module = Mod, state = ExtStateName,
                data = ExtStateData},
            {ok, state, StateData};
        {ok, ExtStateName, ExtStateData, Timeout} ->

            io:fwrite("    {ok, ~w, ~w, ~w}~n",
                [ExtStateName, ExtStateData, Timeout]),
            StateData = #state{module = Mod, state = ExtStateName,
                data = ExtStateData},
            {ok, state, StateData, Timeout};
        {stop, Reason} ->
            io:fwrite("    {stop, ~w}~n", [Reason]),
            {stop, Reason};
        Other ->
            io:fwrite("    ~w~n", [Other]),
            Other
    end.

%% <span class="comment">We use only one state handler for this module.</span>
%% <span class="comment">After being chatty we look up the user's current state</span>
%% <span class="comment">name and call that handler with the current event</span>
%% <span class="comment">and the user's state data.</span>

state(Event, StateData) ->
    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:~w(~w, ~w) ->~n",
        [Mod, ExtStateName, Event, ExtStateData]),
    Result = Mod:ExtStateName(Event, ExtStateData),
    handle_result(Result, state, StateData).

%% <span class="comment">The other gen_fsm callbacks are handled the same as above.</span>

handle_event(Event, StateName, StateData) ->
    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:handle_event(~w, ~w, ~w) ->~n",
        [Mod, Event, ExtStateName, ExtStateData]),
    Result = Mod:handle_event(Event, ExtStateName, ExtStateData),
    handle_result(Result, StateName, StateData).

handle_sync_event(Event, From, StateName, StateData) ->
    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:handle_sync_event(~w, ~w, ~w, ~w) ->~n",
        [Mod, Event, From, ExtStateName, ExtStateData]),
    Result = Mod:handle_sync_event(Event, From, ExtStateName, ExtStateData),
    handle_result(Result, StateName, StateData).

handle_info(Info, StateName, StateData) ->

    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:handle_info(~w, ~w, ~w) ->~n",
        [Mod, Info, ExtStateName, ExtStateData]),
    Result = Mod:handle_info(Info, ExtStateName, ExtStateData),
    handle_result(Result, StateName, StateData).

terminate(Reason, _StateName, StateData) ->
    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:terminate(~w, ~w, ~w) ->~n",
        [Mod, Reason, ExtStateName, ExtStateData]),
    Mod:terminate(Reason, ExtStateName, ExtStateData).

code_change(OldVsn, StateName, StateData, Extra) ->
    Mod = StateData#state.module,
    ExtStateName = StateData#state.state,
    ExtStateData = StateData#state.data,
    io:fwrite("~w:code_change(~w, ~w, ~w, ~w) ->~n",
        [Mod, OldVsn, ExtStateName, ExtStateData, Extra]),
    case Mod:code_change(OldVsn, ExtStateName, ExtStateData, Extra) of
        {ok, NewExtStateName, NewExtStateData} ->
            io:fwrite("    {ok, ~w, ~w}~n",
                NewExtStateName, NewExtStateData),
            NewStateData = StateData#state{state = NewExtStateName,
                data = NewExtStateData},
            {ok, StateName, NewStateData};
        Else ->
            Else
    end.

%% <span class="comment">This function handles the common result set of callbacks.</span>

handle_result({next_state, NewExtStateName, NewExtStateData},
        StateName, StateData) ->
    io:fwrite("    {next_state, ~w, ~w}~n",
        [NewExtStateName, NewExtStateData]),
    NewStateData = StateData#state{state = NewExtStateName,
        data = NewExtStateData},
    {next_state, StateName, NewStateData};
handle_result({next_state, NewExtStateName, NewExtStateData, Timeout},
        StateName, StateData) ->
    io:fwrite("    {next_state, ~w, ~w, ~w}~n",
        [NewExtStateName, NewExtStateData, Timeout]),
    NewStateData = StateData#state{state = NewExtStateName,
        data = NewExtStateData},
    {next_state, StateName, NewStateData, Timeout};
handle_result({reply, Reply, NewExtStateName, NewExtStateData},
        StateName, StateData) ->
    io:fwrite("    {reply, ~w, ~w, ~w}~n",
        [Reply, NewExtStateName, NewExtStateData]),
    NewStateData = StateData#state{state = NewExtStateName,
        data = NewExtStateData},
    {reply, Reply, StateName, NewStateData};
handle_result({reply, Reply, NewExtStateName, NewExtStateData, Timeout},
        StateName, StateData) ->
    io:fwrite("    {reply, ~w, ~w, ~w, ~w}~n",
        [Reply, NewExtStateName, NewExtStateData, Timeout]),
    NewStateData = StateData#state{state = NewExtStateName,
        data = NewExtStateData},
    {reply, Reply, StateName, NewStateData, Timeout};
handle_result({stop, Reason, NewExtStateData}, _StateName, StateData) ->
    io:fwrite("    {stop, ~w, ~w}~n", [Reason, NewExtStateData]),
    NewStateData = StateData#state{data = NewExtStateData},
    {stop, Reason, NewStateData};
handle_result({stop, Reason, Reply, NewExtStateData},
        _StateName, StateData) ->

    io:fwrite("    {stop, ~w, ~w, ~w}~n",
        [Reason, Reply, NewExtStateData]),
    NewStateData = StateData#state{data = NewExtStateData},
    {stop, Reason, Reply, NewStateData};
handle_result(Other, _StateName, _StateData) ->
    io:fwrite("    ~w~n", [Other]),
    Other.

5. Using the New Behaviour

Module

You write a callback module for chatty_fsm behaviours exactly as you would for gen_fsm.

Code listing 5.1: Behaviour Attribute

-behaviour(chatty_fsm).

Code listing 5.2: Required chatty_fsm Callbacks

-export([init/1, handle_event/3, handle_sync_event/4,
         handle_info/3, terminate/3, code_change/4]).

Note: In this example we export an identical interface to the callback

module as imported from gen_fsm. In another example we might have defined

a different list of required callbacks.

Code listing 5.3: State Handler Callbacks

-export([idle/2, busy/2]).

Startup

We can now use chatty_fsm anywhere we would use gen_fsm. It exports the same API as gen_fsm and behaves identically only it adds chattyness.

Code listing 5.4: Starting a chatty_fsm FSM

1> <span class="input">{ok, FSM} = chatty_fsm:start(simple_fsm, [], []).</span>
simple_fsm:init([]) ->
    {ok, idle, {state}}
{ok,<0.31.0>}

Code listing 5.5: Sending an Event

Code listing 5.5: Sending an Event

2> <span class="input">chatty_fsm:send_event(FSM, foo).</span>
simple_fsm:idle(foo, {state}) ->
ok    {next_state, idle, {state}}

You could write a simliar behaviour to do much more interesting things.

6. Conclusion

Cascading Behaviours

It has been shown that behaviour callback modules may also be behaviour modules. You may create a chain of callbacks of indefinite length.

An interesting example would be a complex finite state machine implemented in several behaviour modules such that the outermost had only a few states. Some of these states might be implemented as FSMs in inner behaviour modules.