diff options
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 305 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 582 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 6 | ||||
-rw-r--r-- | lib/tools/emacs/erlang-skels.el | 12 | ||||
-rw-r--r-- | system/doc/design_principles/statem.xml | 194 |
5 files changed, 639 insertions, 460 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 944e9ab13b..aa34f53d29 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -54,7 +54,8 @@ <p> This is a new behavior in Erlang/OTP 19.0. It has been thoroughly reviewed, is stable enough - to be used by at least two heavy OTP applications, and is here to stay. + to be used by at least two heavy OTP applications, + and is here to stay. Depending on user feedback, we do not expect but can find it necessary to make minor not backward compatible changes into Erlang/OTP 20.0. @@ -70,7 +71,7 @@ <item>The state can be any term.</item> <item>Events can be postponed.</item> <item>Events can be self-generated.</item> - <item>Automatic state entry events can be generated.</item> + <item>Automatic state enter code can be called.</item> <item>A reply can be sent from a later state.</item> <item>There can be multiple <c>sys</c> traceable replies.</item> </list> @@ -195,10 +196,14 @@ erlang:'!' -----> Module:StateName/3 to force processing an inserted event before others. </p> <p> - The <c>gen_statem</c> engine can automatically insert - a special event whenever a new state is entered; see - <seealso marker="#type-state_entry_mode"><c>state_entry_mode()</c></seealso>. - This makes it easy to handle code common to all state entries. + The <c>gen_statem</c> engine can automatically + make a specialized call to the + <seealso marker="#state_function">state function</seealso> + whenever a new state is entered; see + <seealso marker="#type-state_enter"><c>state_enter()</c></seealso>. + This is for writing code common to all state entries. + Another way to do it is to insert events at state transitions, + but you have to do so everywhere it is needed. </p> <note> <p>If you in <c>gen_statem</c>, for example, postpone @@ -527,6 +532,20 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="callback_mode_result"/> + <desc> + <p> + This is the return type from + <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> + and selects + <seealso marker="#type-callback_mode">callback mode</seealso> + and whether to do + <seealso marker="#type-state_enter">state enter calls</seealso>, + or not. + </p> + </desc> + </datatype> + <datatype> <name name="callback_mode"/> <desc> <p> @@ -558,44 +577,48 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> - <name name="state_entry_mode"/> + <name name="state_enter"/> <desc> <p> - The <em>state entry mode</em> is selected when starting the - <c>gen_statem</c> and after code change - using the return value from + If the state machine should use <em>state enter calls</em> + is selected when starting the <c>gen_statem</c> + and after code change using the return value from <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>. </p> <p> If <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> - returns a list containing <c>state_entry_events</c>, + returns a list containing <c>state_enter</c>, the <c>gen_statem</c> engine will, at every state change, - insert an event of type - <seealso marker="#type-event_type">enter</seealso> - with content <c>OldState</c>. This event will be inserted - before all other events such as those generated by - <seealso marker="#type-action"><c>action()</c></seealso> - <c>next_event</c>. + call the + <seealso marker="#state_function">state function</seealso> + with arguments <c>(enter, OldState, Data)</c>. + This may look like an event but is really a call + performed after the previous state function returned + and before any event is delivered to the new state function. + See + <seealso marker="#Module:StateName/3"><c>Module:StateName/3</c></seealso> + and + <seealso marker="#Module:handle_event/4"><c>Module:handle_event/4</c></seealso>. </p> <p> If <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> - does not return such a list, no state entry events are inserted. + does not return such a list, no state enter calls are done. </p> <p> - No state entry event will be inserted after a + If <seealso marker="#Module:code_change/4"><c>Module:code_change/4</c></seealso> - since transforming the state to a newer version is regarded - as staying in the same state even if the newer version state - should have a different name. + should transform the state to a state with a different + name it is still regarded as the same state so this + does not cause a state enter call. </p> <p> - Note that a state entry event <em>will</em> be inserted - when entering the initial state even though this formally - is not a state change. In this case <c>OldState</c> - will be the same as <c>State</c>, which can not happen - for an actual state change. + Note that a state enter call <em>will</em> be done + right before entering the initial state even though this + formally is not a state change. + In this case <c>OldState</c> will be the same as <c>State</c>, + which can not happen for a subsequent state change. </p> </desc> </datatype> @@ -605,8 +628,7 @@ handle_event(_, _, State, Data) -> <p> Transition options can be set by <seealso marker="#type-action">actions</seealso> - and they modify the following in how - the state transition is done: + and they modify how the state transition is done: </p> <list type="ordered"> <item> @@ -636,17 +658,22 @@ handle_event(_, _, State, Data) -> <seealso marker="#type-action"><c>action()</c></seealso> <c>next_event</c> are inserted in the queue to be processed before - all other events. + other events. </p> </item> <item> <p> - If the state changes or is the initial state, and the - <seealso marker="#type-state_entry_mode"><em>state entry mode</em></seealso> - is <c>state_entry_events</c>, an event of type - <seealso marker="#type-event_type">enter</seealso> - with content <c>OldState</c> is inserted - to be processed before all other events including those above. + If the state changes or is the initial state, and + <seealso marker="#type-state_enter"><em>state enter calls</em></seealso> + are used, the <c>gen_statem</c> calls + the new state function with arguments + <seealso marker="#type-state_enter">(enter, OldState, Data)</seealso>. + If this call returns any + <seealso marker="#type-enter_action"><c>actions</c></seealso> + that sets transition options + they are merged with the current + That is: <c>hibernate</c> and <c>timeout</c> overrides + the current and <c>reply</c> sends a reply. </p> </item> <item> @@ -656,8 +683,9 @@ handle_event(_, _, State, Data) -> is set through <seealso marker="#type-action"><c>action()</c></seealso> <c>timeout</c>, - an event timer can be started or a time-out zero event - can be enqueued. + an event timer is started if the value is less than + <c>infinity</c> or a time-out zero event + is enqueued if the value is zero. </p> </item> <item> @@ -732,7 +760,7 @@ handle_event(_, _, State, Data) -> is processed before any not yet received external event. </p> <p> - Notice that it is not possible or needed to cancel this time-out, + Note that it is not possible or needed to cancel this time-out, as it is cancelled automatically by any other event. </p> </desc> @@ -743,7 +771,10 @@ handle_event(_, _, State, Data) -> <p> These state transition actions can be invoked by returning them from the - <seealso marker="#state_function">state function</seealso>, from + <seealso marker="#state_function">state function</seealso> + when it is called with an + <seealso marker="#type-event_type">event</seealso>, + from <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> or by giving them to <seealso marker="#enter_loop/5"><c>enter_loop/5,6</c></seealso>. @@ -757,8 +788,8 @@ handle_event(_, _, State, Data) -> override any previous of the same type, so the last in the containing list wins. For example, the last - <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso> - overrides any other <c>event_timeout()</c> in the list. + <seealso marker="#type-postpone"><c>postpone()</c></seealso> + overrides any previous <c>postpone()</c> in the list. </p> <taglist> <tag><c>postpone</c></tag> @@ -775,6 +806,53 @@ handle_event(_, _, State, Data) -> as there is no event to postpone in those cases. </p> </item> + <tag><c>next_event</c></tag> + <item> + <p> + Stores the specified <c><anno>EventType</anno></c> + and <c><anno>EventContent</anno></c> for insertion after all + actions have been executed. + </p> + <p> + The stored events are inserted in the queue as the next to process + before any already queued events. The order of these stored events + is preserved, so the first <c>next_event</c> in the containing + list becomes the first to process. + </p> + <p> + An event of type + <seealso marker="#type-event_type"><c>internal</c></seealso> + is to be used when you want to reliably distinguish + an event inserted this way from any external event. + </p> + </item> + </taglist> + </desc> + </datatype> + <datatype> + <name name="enter_action"/> + <desc> + <p> + These state transition actions can be invoked by + returning them from the + <seealso marker="#state_function">state function</seealso>, from + <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> + or by giving them to + <seealso marker="#enter_loop/5"><c>enter_loop/5,6</c></seealso>. + </p> + <p> + Actions are executed in the containing list order. + </p> + <p> + Actions that set + <seealso marker="#type-transition_option">transition options</seealso> + override any previous of the same type, + so the last in the containing list wins. + For example, the last + <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso> + overrides any previous <c>event_timeout()</c> in the list. + </p> + <taglist> <tag><c>hibernate</c></tag> <item> <p> @@ -805,32 +883,6 @@ handle_event(_, _, State, Data) -> to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c>. </p> </item> - <tag><c>reply_action()</c></tag> - <item> - <p> - Replies to a caller. - </p> - </item> - <tag><c>next_event</c></tag> - <item> - <p> - Stores the specified <c><anno>EventType</anno></c> - and <c><anno>EventContent</anno></c> for insertion after all - actions have been executed. - </p> - <p> - The stored events are inserted in the queue as the next to process - before any already queued events. The order of these stored events - is preserved, so the first <c>next_event</c> in the containing - list becomes the first to process. - </p> - <p> - An event of type - <seealso marker="#type-event_type"><c>internal</c></seealso> - is to be used when you want to reliably distinguish - an event inserted this way from any external event. - </p> - </item> </taglist> </desc> </datatype> @@ -838,13 +890,31 @@ handle_event(_, _, State, Data) -> <name name="reply_action"/> <desc> <p> - Replies to a caller waiting for a reply in + This state transition action can be invoked by + returning it from the + <seealso marker="#state_function">state function</seealso>, from + <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> + or by giving it to + <seealso marker="#enter_loop/5"><c>enter_loop/5,6</c></seealso>. + </p> + <p> + It replies to a caller waiting for a reply in <seealso marker="#call/2"><c>call/2</c></seealso>. <c><anno>From</anno></c> must be the term from argument <seealso marker="#type-event_type"><c>{call,<anno>From</anno>}</c></seealso> - to the + in a call to a <seealso marker="#state_function">state function</seealso>. </p> + <p> + Note that using this action from + <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> + or + <seealso marker="#enter_loop/5"><c>enter_loop/5,6</c></seealso> + would be weird on the border of whichcraft + since there has been no earlier call to a + <seealso marker="#state_function">state function</seealso> + in this server. + </p> </desc> </datatype> <datatype> @@ -869,6 +939,27 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="state_function_enter_result"/> + <desc> + <taglist> + <tag><c>next_state</c></tag> + <item> + <p> + The <c>gen_statem</c> does a state transition to + <c><anno>NextStateName</anno></c> + (which can be the same as the current state), + sets <c><anno>NewData</anno></c>, + and executes all <c><anno>Actions</anno></c>. + </p> + </item> + </taglist> + <p> + All these terms are tuples or atoms and this property + will hold in any future version of <c>gen_statem</c>. + </p> + </desc> + </datatype> + <datatype> <name name="handle_event_result"/> <desc> <taglist> @@ -890,6 +981,27 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="handle_event_enter_result"/> + <desc> + <taglist> + <tag><c>next_state</c></tag> + <item> + <p> + The <c>gen_statem</c> does a state transition to + <c><anno>NextState</anno></c> + (which can be the same as the current state), + sets <c><anno>NewData</anno></c>, + and executes all <c><anno>Actions</anno></c>. + </p> + </item> + </taglist> + <p> + All these terms are tuples or atoms and this property + will hold in any future version of <c>gen_statem</c>. + </p> + </desc> + </datatype> + <datatype> <name name="common_state_callback_result"/> <desc> <taglist> @@ -1148,10 +1260,11 @@ handle_event(_, _, State, Data) -> <seealso marker="#type-event_type"><c>{call,<anno>From</anno>}</c></seealso> to the <seealso marker="#state_function">state function</seealso>. - <c><anno>From</anno></c> and <c><anno>Reply</anno></c> - can also be specified using a - <seealso marker="#type-reply_action"><c>reply_action()</c></seealso> - and multiple replies with a list of them. + A reply or multiple replies canalso be sent + using one or several + <seealso marker="#type-reply_action"><c>reply_action()</c></seealso>s + from a + <seealso marker="#state_function">state function</seealso>. </p> <note> <p> @@ -1362,7 +1475,8 @@ handle_event(_, _, State, Data) -> once after server start and after code change, but before the first <seealso marker="#state_function">state function</seealso> - is called. More occasions may be added in future versions + in the current code version is called. + More occasions may be added in future versions of <c>gen_statem</c>. </p> <p> @@ -1380,7 +1494,7 @@ handle_event(_, _, State, Data) -> or a list containing <seealso marker="#type-callback_mode">callback_mode()</seealso> and possibly the atom - <seealso marker="#type-state_entry_mode"><c>state_entry_events</c></seealso>. + <seealso marker="#type-state_enter"><c>state_enter</c></seealso>. </p> <note> <p> @@ -1601,7 +1715,8 @@ handle_event(_, _, State, Data) -> </p> <p> The function is to return <c>Status</c>, a term that - changes the details of the current state and status of + contains the appropriate details + of the current state and status of the <c>gen_statem</c>. There are no restrictions on the form <c>Status</c> can take, but for the <seealso marker="sys#get_status/1"><c>sys:get_status/1,2</c></seealso> @@ -1625,11 +1740,17 @@ handle_event(_, _, State, Data) -> </func> <func> + <name>Module:StateName(enter, OldState, Data) -> + StateFunctionEnterResult + </name> <name>Module:StateName(EventType, EventContent, Data) -> StateFunctionResult </name> - <name>Module:handle_event(EventType, EventContent, - State, Data) -> HandleEventResult + <name>Module:handle_event(enter, OldState, State, Data) -> + HandleEventResult + </name> + <name>Module:handle_event(EventType, EventContent, State, Data) -> + HandleEventResult </name> <fsummary>Handle an event.</fsummary> <type> @@ -1651,9 +1772,17 @@ handle_event(_, _, State, Data) -> <seealso marker="#type-state_function_result">state_function_result()</seealso> </v> <v> + StateFunctionEnterResult = + <seealso marker="#type-state_function_enter_result">state_function_enter_result()</seealso> + </v> + <v> HandleEventResult = <seealso marker="#type-handle_event_result">handle_event_result()</seealso> </v> + <v> + HandleEventEnterResult = + <seealso marker="#type-handle_event_enter_result">handle_event_enter_result()</seealso> + </v> </type> <desc> <p> @@ -1695,6 +1824,24 @@ handle_event(_, _, State, Data) -> see <seealso marker="#type-action"><c>action()</c></seealso>. </p> <p> + When the <c>gen_statem</c> runs with + <seealso marker="#type-state_enter">state enter calls</seealso>, + these functions are also called with arguments + <c>(enter, OldState, ...)</c> whenever the state changes. + In this case there are some restrictions on the + <seealso marker="#type-enter_action">actions</seealso> + that may be returned: + <seealso marker="#type-postpone"><c>postpone()</c></seealso> + and + <seealso marker="#type-action"><c>{next_event,_,_}</c></seealso> + are not allowed. + You may also not change states from this call. + Should you return <c>{next_state,NextState, ...}</c> + with <c>NextState =/= State</c> the <c>gen_statem</c> crashes. + You are advised to use <c>{keep_state,...}</c> or + <c>keep_state_and_data</c>. + </p> + <p> Note the fact that you can use <seealso marker="erts:erlang#throw/1"><c>throw</c></seealso> to return the result, which can be useful. diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 7f437404ed..aedcfc932f 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -47,13 +47,16 @@ %% Type exports for templates -export_type( [event_type/0, - callback_mode/0, + state_name/0, + callback_mode_result/0, state_function_result/0, + state_function_enter_result/0, handle_event_result/0, + handle_event_enter_result/0, action/0]). %% Fix problem for doc build --export_type([state_entry_mode/0,transition_option/0]). +-export_type([transition_option/0]). %%%========================================================================== %%% Interface functions. @@ -72,10 +75,12 @@ -type event_type() :: {'call',From :: from()} | 'cast' | - 'info' | 'timeout' | 'enter' | 'internal'. + 'info' | 'timeout' | 'internal'. +-type callback_mode_result() :: + callback_mode() | [callback_mode() | state_enter()]. -type callback_mode() :: 'state_functions' | 'handle_event_function'. --type state_entry_mode() :: 'state_entry_events'. +-type state_enter() :: 'state_enter'. -type transition_option() :: postpone() | hibernate() | event_timeout(). @@ -109,6 +114,14 @@ 'postpone' | % Set the postpone option {'postpone', Postpone :: postpone()} | %% + %% All 'next_event' events are kept in a list and then + %% inserted at state changes so the first in the + %% action() list is the first to be delivered. + {'next_event', % Insert event as the next to handle + EventType :: event_type(), + EventContent :: term()} | + enter_action(). +-type enter_action() :: 'hibernate' | % Set the hibernate option {'hibernate', Hibernate :: hibernate()} | %% @@ -116,14 +129,7 @@ {'timeout', % Set the event timeout option Time :: event_timeout(), EventContent :: term()} | %% - reply_action() | - %% - %% All 'next_event' events are kept in a list and then - %% inserted at state changes so the first in the - %% action() list is the first to be delivered. - {'next_event', % Insert event as the next to handle - EventType :: event_type(), - EventContent :: term()}. + reply_action(). -type reply_action() :: {'reply', % Reply to a caller From :: from(), Reply :: term()}. @@ -137,6 +143,16 @@ NewData :: data(), Actions :: [action()] | action()} | common_state_callback_result(). +-type state_function_enter_result() :: + {'next_state', % {next_state,NextStateName,NewData,[]} + NextStateName :: state_name(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextStateName :: state_name(), + NewData :: data(), + Actions :: [enter_action()] | enter_action()} | + common_state_callback_result(). + -type handle_event_result() :: {'next_state', % {next_state,NextState,NewData,[]} NextState :: state(), @@ -146,6 +162,16 @@ NewData :: data(), Actions :: [action()] | action()} | common_state_callback_result(). +-type handle_event_enter_result() :: + {'next_state', % {next_state,NextState,NewData,[]} + NextState :: state(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextState :: state(), + NewData :: data(), + Actions :: [enter_action()] | enter_action()} | + common_state_callback_result(). + -type common_state_callback_result() :: 'stop' | % {stop,normal} {'stop', % Stop the server @@ -164,10 +190,10 @@ NewData :: data()} | {'keep_state', % Keep state, change data NewData :: data(), - Actions :: [action()] | action()} | + Actions :: [ActionType] | ActionType} | 'keep_state_and_data' | % {keep_state_and_data,[]} {'keep_state_and_data', % Keep state and data -> only actions - Actions :: [action()] | action()}. + Actions :: [ActionType] | ActionType}. %% The state machine init function. It is called only once and @@ -184,9 +210,7 @@ %% %% It is called once after init/0 and code_change/4 but before %% the first state callback StateName/3 or handle_event/4. --callback callback_mode() -> - callback_mode() | - [callback_mode() | state_entry_mode()]. +-callback callback_mode() -> callback_mode_result(). %% Example state callback for StateName = 'state_name' %% when callback_mode() =:= state_functions. @@ -197,7 +221,11 @@ %% StateName/3 callbacks and terminate/3, so the state name %% 'terminate' is unusable in this mode. -callback state_name( - event_type(), + 'enter', + OldStateName :: state_name(), + Data :: data()) -> + state_function_enter_result(); + (event_type(), EventContent :: term(), Data :: data()) -> state_function_result(). @@ -205,7 +233,12 @@ %% State callback for all states %% when callback_mode() =:= handle_event_function. -callback handle_event( - event_type(), + 'enter', + OldState :: state(), + State :: state(), % Current state + Data :: data()) -> + handle_event_enter_result(); + (event_type(), EventContent :: term(), State :: state(), % Current state Data :: data()) -> @@ -547,7 +580,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), P = Events = [], - Event = {internal,initial_state}, + Event = {internal,init_state}, %% We enforce {postpone,false} to ensure that %% our fake Event gets discarded, thought it might get logged NewActions = @@ -559,7 +592,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> end, S = #{ callback_mode => undefined, - state_entry_events => false, + state_enter => false, module => Module, name => Name, %% The rest of the fields are set from to the arguments to @@ -886,51 +919,13 @@ loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) -> -parse_callback_mode([], CBMode, SEntry) -> - {CBMode,SEntry}; -parse_callback_mode([H|T], CBMode, SEntry) -> - case callback_mode(H) of - true -> - parse_callback_mode(T, H, SEntry); - false -> - case H of - state_entry_events -> - parse_callback_mode(T, CBMode, true); - _ -> - {undefined,SEntry} - end - end; -parse_callback_mode(_, _CBMode, SEntry) -> - {undefined,SEntry}. - -call_callback_mode(S, CallbackMode) -> - case - parse_callback_mode( - if - is_atom(CallbackMode) -> - [CallbackMode]; - true -> - CallbackMode - end, undefined, false) - of - {undefined,_} -> - {error, - {bad_return_from_callback_mode,CallbackMode}, - ?STACKTRACE()}; - {CBMode,SEntry} -> - {ok, - S#{ - callback_mode := CBMode, - state_entry_events := SEntry}} - end. - call_callback_mode(#{module := Module} = S) -> try Module:callback_mode() of CallbackMode -> - call_callback_mode(S, CallbackMode) + call_callback_mode_result(S, CallbackMode) catch CallbackMode -> - call_callback_mode(S, CallbackMode); + call_callback_mode_result(S, CallbackMode); error:undef -> %% Process undef to check for the simple mistake %% of calling a nonexistent state function @@ -948,38 +943,57 @@ call_callback_mode(#{module := Module} = S) -> {Class,Reason,erlang:get_stacktrace()} end. -loop_event( - Parent, Debug, +call_callback_mode_result(S, CallbackMode) -> + case + parse_callback_mode( + if + is_atom(CallbackMode) -> + [CallbackMode]; + true -> + CallbackMode + end, undefined, false) + of + {undefined,_} -> + {error, + {bad_return_from_callback_mode,CallbackMode}, + ?STACKTRACE()}; + {CBMode,StateEnter} -> + {ok, + S#{ + callback_mode := CBMode, + state_enter := StateEnter}} + end. + +parse_callback_mode([], CBMode, StateEnter) -> + {CBMode,StateEnter}; +parse_callback_mode([H|T], CBMode, StateEnter) -> + case callback_mode(H) of + true -> + parse_callback_mode(T, H, StateEnter); + false -> + case H of + state_enter -> + parse_callback_mode(T, CBMode, true); + _ -> + {undefined,StateEnter} + end + end; +parse_callback_mode(_, _CBMode, StateEnter) -> + {undefined,StateEnter}. + +call_state_function( #{callback_mode := undefined} = S, - Events, - State, Data, P, Event, Hibernate) -> - %% This happens after code_change/4 + Type, Content, State, Data) -> case call_callback_mode(S) of {ok,NewS} -> - loop_event( - Parent, Debug, NewS, Events, - State, Data, P, Event, Hibernate); - {Class,Reason,Stacktrace} -> - terminate( - Class, Reason, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + call_state_function(NewS, Type, Content, State, Data); + Error -> + Error end; -loop_event( - Parent, Debug, +call_state_function( #{callback_mode := CallbackMode, module := Module} = S, - Events, - State, Data, P, {Type,Content} = Event, Hibernate) -> - %% - %% If Hibernate is true here it can only be - %% because it was set from an event action - %% and we did not go into hibernation since there - %% were events in queue, so we do what the user - %% might depend on i.e collect garbage which - %% would have happened if we actually hibernated - %% and immediately was awakened - Hibernate andalso garbage_collect(), - %% + Type, Content, State, Data) -> try case CallbackMode of state_functions -> @@ -989,12 +1003,10 @@ loop_event( end of Result -> - loop_event_result( - Parent, Debug, S, Events, State, Data, P, Event, Result) + {ok,Result,S} catch Result -> - loop_event_result( - Parent, Debug, S, Events, State, Data, P, Event, Result); + {ok,Result,S}; error:badarg -> case erlang:get_stacktrace() of [{erlang,apply, @@ -1004,15 +1016,11 @@ loop_event( when CallbackMode =:= state_functions -> %% We get here e.g if apply fails %% due to State not being an atom - terminate( - error, - {undef_state_function,{Module,State,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,State,Args}}, + Stacktrace}; Stacktrace -> - terminate( - error, badarg, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + {error,badarg,Stacktrace} end; error:undef -> %% Process undef to check for the simple mistake @@ -1022,34 +1030,54 @@ loop_event( [{Module,State,[Type,Content,Data]=Args,_} |Stacktrace] when CallbackMode =:= state_functions -> - terminate( - error, - {undef_state_function,{Module,State,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,State,Args}}, + Stacktrace}; [{Module,handle_event,[Type,Content,State,Data]=Args,_} |Stacktrace] when CallbackMode =:= handle_event_function -> - terminate( - error, - {undef_state_function,{Module,handle_event,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,handle_event,Args}}, + Stacktrace}; Stacktrace -> - terminate( - error, undef, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + {error,undef,Stacktrace} end; Class:Reason -> - Stacktrace = erlang:get_stacktrace(), + {Class,Reason,erlang:get_stacktrace()} + end. + +loop_event( + Parent, Debug, S, Events, + State, Data, P, {Type,Content} = Event, Hibernate) -> + %% + %% If Hibernate is true here it can only be + %% because it was set from an event action + %% and we did not go into hibernation since there + %% were events in queue, so we do what the user + %% might depend on i.e collect garbage which + %% would have happened if we actually hibernated + %% and immediately was awakened + Hibernate andalso garbage_collect(), + case call_state_function(S, Type, Content, State, Data) of + {ok,Result,NewS} -> + {NewData,NextState,Actions} = + parse_event_result( + Parent, Debug, NewS, Events, + State, Data, P, Event, + Result, true), + loop_event_actions( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, Actions); + {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, Debug, S, [Event|Events], State, Data, P) end. %% Interpret all callback return variants -loop_event_result( - Parent, Debug, S, Events, State, Data, P, Event, Result) -> +parse_event_result( + _Parent, Debug, S, Events, State, Data, P, Event, + Result, AllowStateChange) -> case Result of stop -> terminate( @@ -1073,30 +1101,22 @@ loop_event_result( reply_then_terminate( exit, Reason, ?STACKTRACE(), Debug, S, Q, State, NewData, P, Replies); - {next_state,NextState,NewData} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, []); - {next_state,NextState,NewData,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions); + {next_state,State,NewData} -> + {NewData,State,[]}; + {next_state,NextState,NewData} when AllowStateChange -> + {NewData,NextState,[]}; + {next_state,State,NewData,Actions} -> + {NewData,State,Actions}; + {next_state,NextState,NewData,Actions} when AllowStateChange -> + {NewData,NextState,Actions}; {keep_state,NewData} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, State, []); + {NewData,State,[]}; {keep_state,NewData,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, State, Actions); + {NewData,State,Actions}; keep_state_and_data -> - loop_event_actions( - Parent, Debug, S, Events, - State, Data, P, Event, State, []); + {Data,State,[]}; {keep_state_and_data,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, Data, P, Event, State, Actions); + {Data,State,Actions}; _ -> terminate( error, @@ -1105,134 +1125,178 @@ loop_event_result( Debug, S, [Event|Events], State, Data, P) end. -loop_event_actions( - Parent, Debug, S, Events, State, NewData, P, Event, NextState, Actions) -> - Postpone = false, % Shall we postpone this event; boolean() +parse_enter_actions(Debug, S, State, Actions, Hibernate, Timeout) -> + Postpone = forbidden, + NextEvents = forbidden, + parse_actions( + Debug, S, State, listify(Actions), + Hibernate, Timeout, Postpone, NextEvents). + +parse_actions(Debug, S, State, Actions) -> + Postpone = false, Hibernate = false, Timeout = undefined, NextEvents = [], - loop_event_actions( - Parent, Debug, S, Events, State, NewData, P, Event, NextState, - if - is_list(Actions) -> - Actions; - true -> - [Actions] - end, - Postpone, Hibernate, Timeout, NextEvents). + parse_actions( + Debug, S, State, listify(Actions), + Hibernate, Timeout, Postpone, NextEvents). %% -%% Process all actions -loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, [Action|Actions], - Postpone, Hibernate, Timeout, NextEvents) -> +parse_actions( + Debug, _S, _State, [], Hibernate, Timeout, Postpone, NextEvents) -> + {ok,Debug,Hibernate,Timeout,Postpone,NextEvents}; +parse_actions( + Debug, S, State, [Action|Actions], + Hibernate, Timeout, Postpone, NextEvents) -> case Action of %% Actual actions {reply,From,Reply} -> case from(From) of true -> NewDebug = do_reply(Debug, S, State, From, Reply), - loop_event_actions( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, Timeout, NextEvents); + parse_actions( + NewDebug, S, State, Actions, + Hibernate, Timeout, Postpone, NextEvents); false -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P) - end; - {next_event,Type,Content} -> - case event_type(Type) of - true -> - NewDebug = - sys_debug(Debug, S, State, {in,{Type,Content}}), - loop_event_actions( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, Timeout, - [{Type,Content}|NextEvents]); - false -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P) + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} end; %% Actions that set options - {postpone,NewPostpone} when is_boolean(NewPostpone) -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - NewPostpone, Hibernate, Timeout, NextEvents); - {postpone,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); - postpone -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - true, Hibernate, Timeout, NextEvents); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, NewHibernate, Timeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + NewHibernate, Timeout, Postpone, NextEvents); {hibernate,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; hibernate -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, true, Timeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + true, Timeout, Postpone, NextEvents); {timeout,infinity,_} -> % Clear timer - it will never trigger - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, undefined, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, undefined, Postpone, NextEvents); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, NewTimeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, NewTimeout, Postpone, NextEvents); {timeout,_,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; infinity -> % Clear timer - it will never trigger - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, undefined, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, undefined, Postpone, NextEvents); Time when is_integer(Time), Time >= 0 -> NewTimeout = {timeout,Time,Time}, - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, NewTimeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, NewTimeout, Postpone, NextEvents); + {postpone,NewPostpone} + when is_boolean(NewPostpone), Postpone =/= forbidden -> + parse_actions( + Debug, S, State, Actions, + Hibernate, Timeout, NewPostpone, NextEvents); + {postpone,_} -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; + postpone when Postpone =/= forbidden -> + parse_actions( + Debug, S, State, Actions, + Hibernate, Timeout, true, NextEvents); + {next_event,Type,Content} -> + case event_type(Type) of + true when NextEvents =/= forbidden -> + NewDebug = + sys_debug(Debug, S, State, {in,{Type,Content}}), + parse_actions( + NewDebug, S, State, Actions, + Hibernate, Timeout, Postpone, + [{Type,Content}|NextEvents]); + _ -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} + end; _ -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} + end. + +loop_event_actions( + Parent, Debug, #{state_enter := StateEnter} = S, Events, + State, NewData, P, Event, NextState, Actions) -> + case parse_actions(Debug, S, State, Actions) of + {ok,NewDebug,Hibernate,Timeout,Postpone,NextEvents} -> + case + StateEnter andalso + ((NextState =/= State) + orelse maps:is_key(init_state, S)) of + true -> + loop_event_enter( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents); + false -> + loop_event_result( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) + end; + {Class,Reason,Stacktrace} -> terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), + Class, Reason, Stacktrace, Debug, S, [Event|Events], State, NewData, P) - end; -%% -%% End of actions list -loop_event_actions( - Parent, Debug, #{state_entry_events := SEEvents} = S, Events, - State, NewData, P0, Event, NextState, [], - Postpone, Hibernate, Timeout, NextEvents) -> + end. + +loop_event_enter( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) -> + case call_state_function(S, enter, State, NextState, NewData) of + {ok,Result,NewS} -> + {NewerData,_,Actions} = + parse_event_result( + Parent, Debug, NewS, Events, + NextState, NewData, P, Event, + Result, false), + loop_event_enter_actions( + Parent, Debug, NewS, Events, + State, NewerData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents, Actions); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events], NextState, NewData, P) + end. + +loop_event_enter_actions( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents, Actions) -> + case + parse_enter_actions(Debug, S, NextState, Actions, Hibernate, Timeout) + of + {ok,NewDebug,NewHibernate,NewTimeout,_,_} -> + loop_event_result( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + NewHibernate, NewTimeout, Postpone, NextEvents); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events], NextState, NewData, P) + end. + +loop_event_result( + Parent, Debug, S, Events, + State, NewData, P0, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) -> %% %% All options have been collected and next_events are buffered. %% Do the actual state transition. @@ -1252,44 +1316,21 @@ loop_event_actions( {lists:reverse(P1, Events),[]} end, %% Place next events first in queue - Q3 = lists:reverse(NextEvents, Q2), - %% State entry events - Q = - case SEEvents of - true -> - %% Generate state entry events - case - (NextState =/= State) - orelse maps:is_key(init_state, S) - of - true -> - %% State change or initial state - [{enter,State}|Q3]; - false -> - Q3 - end; - false -> - Q3 - end, + Q = lists:reverse(NextEvents, Q2), %% NewDebug = sys_debug( Debug, S, State, case Postpone of true -> - {postpone,Event,NextState}; + {postpone,Event,State}; false -> - {consume,Event,NextState} + {consume,Event,State} end), loop_events( Parent, NewDebug, %% Avoid infinite loop in initial state with state entry events - case maps:is_key(init_state, S) of - true -> - maps:remove(init_state, S); - false -> - S - end, + maps:remove(init_state, S), Q, NextState, NewData, P, Hibernate, Timeout). %%--------------------------------------------------------------------------- @@ -1369,7 +1410,7 @@ error_info( Class, Reason, Stacktrace, #{name := Name, callback_mode := CallbackMode, - state_entry_events := SEEvents}, + state_enter := StateEnter}, Q, P, FmtData) -> {FixedReason,FixedStacktrace} = case Stacktrace of @@ -1397,9 +1438,9 @@ error_info( _ -> {Reason,Stacktrace} end, CBMode = - case SEEvents of + case StateEnter of true -> - [CallbackMode,state_entry_events]; + [CallbackMode,state_enter]; false -> CallbackMode end, @@ -1471,3 +1512,8 @@ format_status_default(Opt, State, Data) -> _ -> [{data,[{"State",StateData}]}] end. + +listify(Item) when is_list(Item) -> + Item; +listify(Item) -> + [Item]. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index eef8f265c4..48f93b1de7 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -37,7 +37,7 @@ all() -> {group, stop_handle_event}, {group, abnormal}, {group, abnormal_handle_event}, - shutdown, stop_and_reply, enter_events, event_order, code_change, + shutdown, stop_and_reply, state_enter, event_order, code_change, {group, sys}, hibernate, enter_loop]. @@ -582,7 +582,7 @@ stop_and_reply(_Config) -> -enter_events(_Config) -> +state_enter(_Config) -> process_flag(trap_exit, true), Self = self(), @@ -615,7 +615,7 @@ enter_events(_Config) -> end}, {ok,STM} = gen_statem:start_link( - ?MODULE, {map_statem,Machine,[state_entry_events]}, []), + ?MODULE, {map_statem,Machine,[state_enter]}, []), [{enter,start,start,1}] = flush(), {echo,start,2} = gen_statem:call(STM, echo), diff --git a/lib/tools/emacs/erlang-skels.el b/lib/tools/emacs/erlang-skels.el index 0284c9d686..95cc989c73 100644 --- a/lib/tools/emacs/erlang-skels.el +++ b/lib/tools/emacs/erlang-skels.el @@ -904,7 +904,7 @@ Please see the function `tempo-define-template'.") "%% @doc" n "%% Define the callback_mode() for this callback module." n (erlang-skel-separator-end 2) - "-spec callback_mode() -> gen_statem:callback_mode()." n + "-spec callback_mode() -> gen_statem:callback_mode_result()." n "callback_mode() -> state_functions." n n (erlang-skel-separator-start 2) @@ -936,6 +936,10 @@ Please see the function `tempo-define-template'.") "%% instead of StateName/3 functions like this!" n (erlang-skel-separator-end 2) "-spec state_name(" n> + "'enter', OldState :: gen_statem:state_name()," n> + "Data :: term()) ->" n> + "gen_statem:state_function_enter_result();" n + " (" n> "gen_statem:event_type(), Msg :: term()," n> "Data :: term()) ->" n> "gen_statem:state_function_result()." n @@ -1015,7 +1019,7 @@ Please see the function `tempo-define-template'.") "%% @doc" n "%% Define the callback_mode() for this callback module." n (erlang-skel-separator-end 2) - "-spec callback_mode() -> gen_statem:callback_mode()." n + "-spec callback_mode() -> gen_statem:callback_mode_result()." n "callback_mode() -> handle_event_function." n n (erlang-skel-separator-start 2) @@ -1044,6 +1048,10 @@ Please see the function `tempo-define-template'.") "%% StateName/3 functions are called instead!" n (erlang-skel-separator-end 2) "-spec handle_event(" n> + "'enter', OldState :: term()," n> + "State :: term(), Data :: term()) ->" n> + "gen_statem:handle_event_enter_result();" n + " (" n> "gen_statem:event_type(), Msg :: term()," n> "State :: term(), Data :: term()) ->" n> "gen_statem:handle_event_result()." n diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 565b0e5274..d2a9b23570 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -195,21 +195,19 @@ handle_event(EventType, EventContent, State, Data) -> <!-- =================================================================== --> <section> - <marker id="state_entry_events" /> - <title>State Entry Events</title> + <marker id="state_enter" /> + <title>State Enter Calls</title> <p> The <c>gen_statem</c> behavior can regardless of callback mode - automatically generate an - <seealso marker="stdlib:gen_statem#type-state_entry_mode"> - event whenever the state changes - </seealso> - so you can write state entry code + automatically call the state function + with special arguments whenever the state changes + so you can write state entry actions near the rest of the state transition rules. It typically looks like this: </p> <pre> StateName(enter, _OldState, Data) -> - ... code for state entry here ... + ... code for state entry actions here ... {keep_state, NewData}; StateName(EventType, EventContent, Data) -> ... code for actions here ... @@ -217,7 +215,7 @@ StateName(EventType, EventContent, Data) -> <p> Depending on how your state machine is specified, this can be a very useful feature, but if you use it - you will have to handle the state entry events in all states. + you will have to handle the state enter call in all states. </p> </section> @@ -751,12 +749,6 @@ stop() -> Generated by any regular process message sent to the <c>gen_statem</c> process. </item> - <tag><c>enter</c></tag> - <item> - Generated by a state transition with - <c>OldState =/= NewState</c> when running with - <seealso marker="#state_entry_events">state entry events</seealso>. - </item> <tag><c>timeout</c></tag> <item> Generated by state transition action @@ -972,63 +964,35 @@ do_unlock() -> <!-- =================================================================== --> <section> - <title>Self-Generated Events</title> - <p> - It can sometimes be beneficial to be able to generate events - to your own state machine. - This can be done with the state transition - <seealso marker="stdlib:gen_statem#type-action">action</seealso> - <c>{next_event,EventType,EventContent}</c>. - </p> + <title>State Entry Actions</title> <p> - You can generate events of any existing - <seealso marker="stdlib:gen_statem#type-action">type</seealso>, - but the <c>internal</c> type can only be generated through action - <c>next_event</c>. Hence, it cannot come from an external source, - so you can be certain that an <c>internal</c> event is an event - from your state machine to itself. + Say you have a state machine specification + that uses state entry actions. + Allthough you can code this using self-generated events + (described in the next section), especially if just + one or a few states has got state entry actions, + this is a perfect use case for the built in + <seealso marker="#state_enter">state enter calls</seealso>. </p> <p> - One example for this is to pre-process incoming data, for example - decrypting chunks or collecting characters up to a line break. - This could be modelled with a separate state machine that sends - the pre-processed events to the main state machine, or to decrease - overhead the small pre-processing state machine can be implemented - in the common state event handling of the main state machine - using a few state data variables and then send the pre-processed - events as internal events to the main state machine. - </p> - <p> - Another example of using self-generated events can be when you have - a state machine specification that uses state entry actions. - You can code that using a dedicated function - to do the state transition. But if you want that code to be - visible besides the other state logic, you can insert - an <c>internal</c> event that does the entry actions. - This has the same unfortunate consequence as using - state transition functions: everywhere you go to - the state, you must explicitly - insert the <c>internal</c> event - or use a state transition function. - This is something that can be forgotten, and if you find that - annoying please look at the next chapter. - </p> - <p> - The following is an implementation of entry actions - using <c>internal</c> events with content <c>enter</c> - using a helper function <c>enter/3</c> for state entry: + You return a list containing <c>state_enter</c> from your + <seealso marker="stdlib:gen_statem#Module:callback_mode/0"><c>callback_mode/0</c></seealso> + function and the <c>gen_statem</c> engine will call your + state function once with the arguments + <c>(enter, OldState, ...)</c> whenever the state changes. + Then you just need to handle these event-like calls in all states. </p> <code type="erl"><![CDATA[ ... init(Code) -> process_flag(trap_exit, true), Data = #{code => Code}, - enter(ok, locked, Data). + {ok, locked, Data}. callback_mode() -> - state_functions. + [state_functions,state_enter]. -locked(internal, enter, Data) -> +locked(enter, _OldState, Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; locked( @@ -1036,79 +1000,94 @@ locked( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> - enter(next_state, open, Data); + {next_state, open, Data}; ... -open(internal, enter, Data) -> +open(enter, _OldState, Data) -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer => Tref}}; open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - enter(next_state, locked, Data); + {next_state, locked, Data}; ... - -enter(Tag, State, Data) -> - {Tag,State,Data,[{next_event,internal,enter}]}. ]]></code> </section> <!-- =================================================================== --> <section> - <title>Using State Entry Events</title> + <title>Self-Generated Events</title> <p> - Here is the same example as the previous but instead using - the built in - <seealso marker="#state_entry_events">state entry events</seealso>. + It can sometimes be beneficial to be able to generate events + to your own state machine. + This can be done with the state transition + <seealso marker="stdlib:gen_statem#type-action">action</seealso> + <c>{next_event,EventType,EventContent}</c>. </p> <p> - Since the state entry events are unconditionally inserted by - the <c>gen_statem</c> engine you can not forget to insert them - yourself and you will have to handle the state entry events - in every state. + You can generate events of any existing + <seealso marker="stdlib:gen_statem#type-action">type</seealso>, + but the <c>internal</c> type can only be generated through action + <c>next_event</c>. Hence, it cannot come from an external source, + so you can be certain that an <c>internal</c> event is an event + from your state machine to itself. </p> <p> - If you want state entry code in just a few states the previous - example may be more suitable, especially to only send internal - events when entering just those few states. - Note: additional discipline will be required. + One example for this is to pre-process incoming data, for example + decrypting chunks or collecting characters up to a line break. + Purists may argue that this should be modelled with a separate + state machine that sends pre-processed events + to the main state machine. + But to decrease overhead the small pre-processing state machine + can be implemented in the common state event handling + of the main state machine using a few state data variables + that then sends the pre-processed events as internal events + to the main state machine. </p> <p> - You can also in the previous example choose to generate - events looking just like the events you get from using - <seealso marker="#state_entry_events">state entry events</seealso>. - This may be confusing, or practical, - depending on your point of view. + The following example use an input model where you give the lock + characters with <c>put_chars(Chars)</c> and then call + <c>enter()</c> to finish the input. </p> <code type="erl"><![CDATA[ ... -init(Code) -> - process_flag(trap_exit, true), - Data = #{code => Code}, - {ok, locked, Data}. +-export(put_chars/1, enter/0). +... +put_chars(Chars) when is_binary(Chars) -> + gen_statem:call(?NAME, {chars,Chars}). -callback_mode() -> - [state_functions,state_entry_events]. +enter() -> + gen_statem:call(?NAME, enter). + +... locked(enter, _OldState, Data) -> do_lock(), - {keep_state,Data#{remaining => Code}}; -locked( - cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> - {next_state, open, Data}; + {keep_state,Data#{remaining => Code, buf => []}}; ... -open(enter, _OldState, Data) -> - Tref = erlang:start_timer(10000, self(), lock), - do_unlock(), - {keep_state,Data#{timer => Tref}}; -open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - {next_state, locked, Data}; +handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) -> + {keep_state, Data#{buf := [Chars|Buf], + [{reply,From,ok}]}; +handle_event({call,From}, enter, #{buf := Buf} = Data) -> + Chars = unicode:characters_to_binary(lists:reverse(Buf)), + try binary_to_integer(Chars) of + Digit -> + {keep_state, Data#{buf := []}, + [{reply,From,ok}, + {next_event,internal,{button,Chars}}]} + catch + error:badarg -> + {keep_state, Data#{buf := []}, + [{reply,From,{error,not_an_integer}}]} + end; ... ]]></code> + <p> + If you start this program with <c>code_lock:start([17])</c> + you can unlock with <c>code_lock:put_chars(<<"001">>), + code_lock:put_chars(<<"7">>), code_lock:enter()</c>. + </p> </section> <!-- =================================================================== --> @@ -1117,7 +1096,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> <title>Example Revisited</title> <p> This section includes the example after all mentioned modifications - and some more using the entry actions, + and some more using state enter calls, which deserves a new state diagram: </p> <image file="../design_principles/code_lock_2.png"> @@ -1163,7 +1142,7 @@ init(Code) -> {ok, locked, Data}. callback_mode() -> - [state_functions,state_entry_events]. + [state_functions,state_enter]. locked(enter, _OldState, #{code := Code} = Data) -> do_lock(), @@ -1215,8 +1194,7 @@ code_change(_Vsn, State, Data, _Extra) -> This section describes what to change in the example to use one <c>handle_event/4</c> function. The previously used approach to first branch depending on event - does not work that well here because of - the state entry events, + does not work that well here because of the state enter calls, so this example first branches depending on state: </p> <code type="erl"><![CDATA[ @@ -1225,7 +1203,7 @@ code_change(_Vsn, State, Data, _Extra) -> ... callback_mode() -> - [handle_event_function,state_entry_events]. + [handle_event_function,state_enter]. %% State: locked handle_event(enter, _OldState, locked, #{code := Code} = Data) -> @@ -1413,7 +1391,7 @@ init({Code,LockButton}) -> {ok, {locked,LockButton}, Data}. callback_mode() -> - [handle_event_function,state_entry_events]. + [handle_event_function,state_enter]. handle_event( {call,From}, {set_lock_button,NewLockButton}, |