From 2591124991bf601321d44de37d6c32e49075e68f Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 18 Apr 2016 15:39:06 +0200 Subject: Introduce Fred Herbert suggested additions --- system/doc/design_principles/statem.xml | 247 +++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 4 deletions(-) diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 02754bd23d..26e4840640 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -71,6 +71,7 @@ State(S) x Event(E) -> Actions(A), State(S') Since A and S' depend only on S and E the kind of state machine described here is a Mealy Machine. + (See for example the corresponding Wikipedia article)

Like most gen_ behaviours, gen_statem keeps @@ -81,9 +82,19 @@ State(S) x Event(E) -> Actions(A), State(S') a state machine implemented with this behaviour Turing complete. But it feels mostly like an Event Driven Mealy Machine.

+ + + + +
+ + Callback Modes

The gen_statem behaviour supports two different - callback modes. In the mode state_functions, + + callback modes. + + In the mode state_functions, the state transition rules are written as a number of Erlang functions, which conform to the following convention:

@@ -109,6 +120,64 @@ handle_event(EventType, EventContent, State, Data) -> execute state transition actions on the machine engine itself and send replies.

+ +
+ Choosing Callback Mode +

+ The two + + callback modes + + gives different possibilities + and restrictions, but one goal remains: + you want to handle all possible combinations of + events and states. +

+

+ You can for example do this by focusing on one state at the time + and for every state ensure that all events are handled, + or the other way around focus on one event at the time + and ensure that it is handled in every state, + or mix these strategies. +

+

+ With state_functions you are restricted to use + atom only states, and the gen_statem engine dispatches + on state name for you. This encourages the callback module + to gather the implementation of all event actions particular + to one state in the same place in the code + hence focus on one state at the time. +

+

+ This mode fits well when you have a regular state diagram + like the ones in this chapter that describes all events and actions + belonging to a state visually around that state, + and each state has its unique name. +

+

+ With handle_event_function you are free to mix strategies + as you like because all events and states + are handled in the the same callback function. +

+

+ This mode works equally well when you want to focus on + one event at the time or when you want to focus on + one state at the time, but the handle_event/4 function + quickly grows too large to handle without introducing dispatching. +

+

+ The mode enables the use of non-atom states for example + complex states or even hiearchical states. + If, for example, a state diagram is largely alike + for the client and for the server side of a protocol; + then you can have a state {StateName,server} or + {StateName,client} and since you do the dispatching + yourself you make StateName decide where in the code + to handle most events in the state. + The second element of the tuple is then used to select + whether to handle special client side or server side events. +

+
@@ -773,6 +842,20 @@ open(cast, {button,_}, Data) -> {keep_state,Data,[postpone]}; ... ]]> +

+ The fact that a postponed event is only retried after a state change + translates into a requirement on the event and state space: + if you have a choice between storing a state data item + in the State or in the Data; + should a change in the item value affect which events that + are handled, then this item ought to be part of the state. +

+

+ What you want to avoid is that you maybe much later decide + to postpone an event in one state and by misfortune it is never retried + because the code only changes the Data but not the State. +

+
Fuzzy State Diagrams

@@ -788,6 +871,7 @@ open(cast, {button,_}, Data) -> as in postpone it.

+
Selective Receive

@@ -956,6 +1040,7 @@ enter(Tag, State, Data) -> Nor does it show that the code_length/0 call shall be handled in every state.

+
Callback Mode: state_functions

@@ -1029,6 +1114,7 @@ code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}. ]]>

+
Callback Mode: handle_event_function

@@ -1059,7 +1145,7 @@ handle_event( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data, []); + enter(next_state, open, Data); [Digit|Rest] -> % Incomplete {keep_state,Data#{remaining := Rest}}; [_|_] -> % Wrong @@ -1072,7 +1158,7 @@ handle_event(internal, enter, open, Data) -> do_unlock(), {keep_state,Data#{timer => Tref}}; handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) -> - enter(next_state, locked, Data, []); + enter(next_state, locked, Data); handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; %% @@ -1086,8 +1172,161 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->

Note that postponing buttons from the locked state to the open state feels like the wrong thing to do - for a code lock, but it illustrates event postponing. + for a code lock, but it at least illustrates event postponing.

+ + +
+ Complex State +

+ The + + callback mode + + handle_event_function + enables using a non-atom state as described in + + Callback Modes, + + for example a complex state term like a tuple. +

+

+ One reason to use this is when you have + a state item that affects the event handling + in particular when combining that with postponing events. + Let us complicate the previous example + by introducing a configurable lock button + (this is the state item in question) + that in the open state immediately locks the door, + and an API function set_lock_button/1 to set the lock button. +

+

+ Suppose now that we call set_lock_button + while the door is open, + and have already postponed a button event + that up until now was not the lock button; + the sensible thing might be to say that + the button was pressed too early so it should + not be recognized as the lock button, + but then it might be surprising that a button event + that now is the lock button event arrives (as retried postponed) + immediately after the state transits to locked. +

+

+ So let us make the button/1 function synchronous + by using gen_statem:call, + and still postpone its events in the open state. + Then a call to button/1 during the open + state will not return until the state transits to locked + since it is there the event is handled and the reply is sent. +

+

+ If now one process calls set_lock_button/1 + to change the lock button while some other process + hangs in button/1 with the new lock button + it could be expected that the hanging lock button call + immediately takes effect and locks the lock. + Therefore we make the current lock button a part of the state + so when we change the lock button the state will change + and all postponed events will be retried. +

+

+ We define the state as {StateName,LockButton} + where StateName is as before + and LockButton is the current lock button: +

+ + gen_statem:start_link( + {local,?NAME}, ?MODULE, {Code,LockButton}, []). +stop() -> + gen_statem:stop(?NAME). + +button(Digit) -> + gen_statem:call(?NAME, {button,Digit}). +code_length() -> + gen_statem:call(?NAME, code_length). +set_lock_button(LockButton) -> + gen_statem:call(?NAME, {set_lock_button,LockButton}). + +init({Code,LockButton}) -> + process_flag(trap_exit, true), + Data = #{code => Code}, + enter(handle_event_function, {locked,LockButton}, Data, []). + +%% State: locked +handle_event(internal, enter, {locked,_}, #{code := Code} = Data) -> + do_lock(), + {keep_state,Data#{remaining => Code}}; +handle_event( + {call,From}, {button,Digit}, {locked,LockButton}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> % Complete + enter(next_state, {open,LockButton}, Data, [{reply,From,ok}]); + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest},[{reply,From,ok}]}; + [_|_] -> % Wrong + {keep_state,Data#{remaining := Code},[{reply,From,ok}]} + end; +%% +%% State: open +handle_event(internal, enter, {open,_}, Data) -> + Tref = erlang:start_timer(10000, self(), lock), + do_unlock(), + {keep_state,Data#{timer => Tref}}; +handle_event( + info, {timeout,Tref,lock}, {open,LockButton}, + #{timer := Tref} = Data) -> + enter(next_state, {locked,LockButton}, Data, []); +handle_event( + {call,From}, {button,LockButton}, {open,LockButton}, + #{timer := Tref} = Data) -> + erlang:cancel_timer(Tref), + enter(next_state, {locked,LockButton}, Data, [{reply,From,locked}]); +handle_event({call,_}, {button,_}, {open,_}, _) -> + {keep_state_and_data,[postpone]}; +%% +%% Any state +handle_event( + {call,From}, {set_lock_button,NewLockButton}, {StateName,OldLockButton}, + Data) -> + {next_state,{StateName,NewLockButton},Data, + [{reply,From,OldLockButton}]}; +handle_event({call,From}, code_length, _State, #{code := Code}) -> + {keep_state_and_data,[{reply,From,length(Code)}]}. + +enter(Tag, State, Data, Actions) -> + {Tag,State,Data,[{next_event,internal,enter}|Actions]}. + +do_lock() -> + io:format("Locked~n", []). +do_unlock() -> + io:format("Open~n", []). + +terminate(_Reason, State, _Data) -> + State =/= locked andalso do_lock(), + ok. +code_change(_Vsn, State, Data, _Extra) -> + {ok,State,Data}. + ]]> +

+ It may be an ill-fitting model for a physical code lock + that the button/1 call might hang until the lock + is locked. But for an API in general it is really not + that strange. +

+
+ -- cgit v1.2.3