From 04d40c5cd18aca449606c19608e8044f593ee99e Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 22 Sep 2016 17:40:47 +0200 Subject: Change state entry events into state enter calls --- system/doc/design_principles/statem.xml | 194 ++++++++++++++------------------ 1 file changed, 86 insertions(+), 108 deletions(-) (limited to 'system') 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) ->
- - State Entry Events + + State Enter Calls

The gen_statem behavior can regardless of callback mode - automatically generate an - - event whenever the state changes - - 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:

 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) ->
     

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.

@@ -751,12 +749,6 @@ stop() -> Generated by any regular process message sent to the gen_statem process. - enter - - Generated by a state transition with - OldState =/= NewState when running with - state entry events. - timeout Generated by state transition action @@ -972,63 +964,35 @@ do_unlock() ->
- Self-Generated Events -

- It can sometimes be beneficial to be able to generate events - to your own state machine. - This can be done with the state transition - action - {next_event,EventType,EventContent}. -

+ State Entry Actions

- You can generate events of any existing - type, - but the internal type can only be generated through action - next_event. Hence, it cannot come from an external source, - so you can be certain that an internal 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 + state enter calls.

- 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. -

-

- 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 internal 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 internal 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. -

-

- The following is an implementation of entry actions - using internal events with content enter - using a helper function enter/3 for state entry: + You return a list containing state_enter from your + callback_mode/0 + function and the gen_statem engine will call your + state function once with the arguments + (enter, OldState, ...) whenever the state changes. + Then you just need to handle these event-like calls in all states.

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}]}. ]]>
- Using State Entry Events + Self-Generated Events

- Here is the same example as the previous but instead using - the built in - state entry events. + It can sometimes be beneficial to be able to generate events + to your own state machine. + This can be done with the state transition + action + {next_event,EventType,EventContent}.

- Since the state entry events are unconditionally inserted by - the gen_statem 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 + type, + but the internal type can only be generated through action + next_event. Hence, it cannot come from an external source, + so you can be certain that an internal event is an event + from your state machine to itself.

- 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.

- You can also in the previous example choose to generate - events looking just like the events you get from using - state entry events. - 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 put_chars(Chars) and then call + enter() to finish the input.

- 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; ... ]]> +

+ If you start this program with code_lock:start([17]) + you can unlock with code_lock:put_chars(<<"001">>), + code_lock:put_chars(<<"7">>), code_lock:enter(). +

@@ -1117,7 +1096,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> Example Revisited

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:

@@ -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 handle_event/4 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:

... 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}, -- cgit v1.2.3