Erlang/Elixir erlang 与状态机

helapu · July 20, 2017 · Last by helapu replied at July 23, 2017 · 6850 hits

ruby 里面的aasm是最常用的有限状态机。

(因为 erlang 的文档好像是 SPA 一样的,所以没发贴出具体某一部分的文档。erlang/OTP Doc。)这个对 SPA 的描述是错的,SPA 可以非常清晰用 url 反应 web 的内容。

相关链接 gen_fsm,

在 erlang 的标准库里面使用 gen_fsm 行为模式用来处理有限状态机,但是在 Erlang/OTP 19.0 开始增加了 gen_statem

gen_statem 作为 gen_fsm 的替代品,拥有完全的功能,并且增加了类似无限状态机(我也不确定这样说对不对),这个行为模式已经在 Erlang/OTP 20.0 确定下来了不会改变了(当前版本就是 Erlang/OTP 20.0 2017/07/20)

状态机的模式有两种

Module:callback_mode() -> CallbackMode
callback_mode() = state_functions | handle_event_function

如果是 state_functions 那就是和 gen_fsm 一样的有限状态机,callback 函数和状态的原子是一样,总共两种

Module:StateName(enter, OldState, Data) -> StateEnterResult(StateName) 
Module:StateName(EventType, EventContent, Data) -> StateFunctionResult

如果是 handle_event_function 那就是类似无限状态了 有一个 event 来处理

Module:handle_event(enter, OldState, State, Data) -> StateEnterResult(State) 
Module:handle_event(EventType, EventContent, State, Data) -> HandleEventResult

StateName就是自定义的一些状态的原子,任意的原子。比如 hello, hi, open, closed, withdraw, topup 等等

使用如下

%% code_lock:dump().

dump() ->
    code_lock:start_link([1,2,3]),
    code_lock:button(1),
    timer:sleep(1000),
    code_lock:get_state(),
    code_lock:button(2),
    timer:sleep(1000),
    code_lock:button(3).

在有限状态之间转换的时候,可以跳转到下一个状态、不修改当前状态或者数据

 在这些数据类型下面可以找到具体的使用 fang

state_enter_result(State) 
event_handler_result(StateType) 

 #### 有限状态机

-module(code_lock).
-define(NAME, code_lock).

-behaviour(gen_statem).

%% API
-export([start_link/1, button/1, stop/0, get_state/0, dump/0]).
%% gen_statem callback
-export([init/1, callback_mode/0, terminate/3, code_change/4]).
%% 状态转换 一共两种状态 [locked, open]
-export([locked/3, open/3]).

%% API

dump() ->
    code_lock:start_link([1,2,3]),
    code_lock:button(1),
    timer:sleep(1000),
    code_lock:get_state(),
    code_lock:button(2),
    timer:sleep(1000),
    code_lock:button(3).

start_link(Code) ->
    gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
%% 输入密码
button(Digit) ->
    io:format("输入按键(一个数字):~w ~n", [Digit]),
    gen_statem:cast(?NAME, {button,Digit}).
%% 查看当前状态
get_state() ->
    gen_statem:call(?NAME, get_state).
%% 停止
stop() ->
    gen_statem:call(?NAME, stop).

%% gen_statem callback
init(Code) ->
    do_lock(),
    % code为存储的密码 remaining为剩下的需要输入的密码
    Data = #{code => Code, remaining => Code},
    % 初始化为锁定状态
    {ok, locked, Data}.
%% 模式 state_functions | process_event_function
callback_mode() ->
    state_functions.

terminate(_Reason, State, _Data) ->
    State =/= locked andalso do_lock(),
    ok.
code_change(_Vsn, State, Data, _Extra) ->
    {ok, State, Data}.

%% 处理locked状态下的条件变化 当前为locked状态
locked(cast, {button,Digit}, Data0) ->
    case analyze_lock(Digit, Data0) of
        {open = StateName, Data} ->
              {next_state, StateName, Data, 10000}; % {next_state, open, Data, 10000};
          {locked = StateName, Data} ->
              {next_state, StateName, Data};
        {_StateName, Data} ->
              {next_state, locked, Data}
    end;
locked({call, From}, get_state, _Data) ->
    process_state(From, locked);
locked({info, Msg}, StateName, Data) ->
    io:format("locked info ~w", [Data]),
    process_info(Msg, StateName, Data).

%% 处理open状态下的条件变化 当前为open状态
open({call, From}, get_state, _Data) ->
    process_state(From, open);
open(timeout, _, Data) ->
    io:format("open state timeout ~n", []),
    do_lock(),
    {next_state, locked, Data};
open(cast, {button,_}, Data) ->
    io:format("open to locked ~w ~n", [Data]),
    {next_state, locked, Data};
open({call, From}, Msg, Data) ->
    process_call(From, Msg, Data);
open(info, Msg, Data) ->
    process_info(Msg, open, Data).

%% 辅助function
process_state(From, StateName) ->
    io:format("当前状态:~w ~n", [StateName]),
    {keep_state_and_data, {reply, From, ok}}.
process_call(From, stop, Data) ->
    {stop_and_reply, normal,  {reply, From, ok}, Data}.
process_info(Info, StateName, Data) ->
    {stop, {shutdown, {unexpected, Info, StateName}}, StateName, Data}.
% 验证密码
analyze_lock(Digit, #{code := Code, remaining := Remaining} = Data) ->
    io:format("analyze Digit: ~w  Remaining:~w ~n", [Digit,Remaining]),
    case Remaining of
        [Digit] -> %密码最后一位正确
            io:format("last one password ok~n", []),
              do_unlock(),
              {open,  Data#{remaining := Code}};
        [Digit|Rest] -> %依次输入密码
            io:format("Rest: ~w~n", [Rest]),
            {locked, Data#{remaining := Rest}};
        _Wrong -> %密码错误
            io:format("password wrong~n", []),
            {locked, Data#{remaining := Code}}
    end.

do_lock() ->
    io:format("Lock~n", []).
do_unlock() ->
    io:format("Unlock~n", []).

handle event

state machine

跟你挑个刺,第二段话暴露了你对 SPA 的极大不了解(同时也侧面反映了现在做 SPA 的菜鸡太多),只有路由方面没做好的 SPA 才会出现你说的这种情况,做得好的话任何情况(哪怕是某个极深的路径下打开了一个 modal)都可以通过 URL mapping 还原出 UI 的状态来。

此外,貌似你对 erlang 的文档也有误解?modules 是可以检索出具体的 URL 的。这是 gen_fsm 的:http://erlang.org/doc/man/gen_fsm.html,这是 gen_statem 的:http://erlang.org/doc/man/gen_statem.html

这两个模块是有文档链接的,LZ 可以更新一下文章:

Reply to nightire

已经增加了链接,我忘记了当时是如何查看 erlang 文档的。查看文档的时候,url 没有变化,我也很奇怪这一点

对于 SPA,我确实有这样的印象,再去更新下这方面的知识吧。

Reply to darkbaby123

已经更新了 😄

我怎么才能重现一个 bug 呢 macOS 和 chrome 都是最新版本,在对上面的文档进行编辑的时候,在文档的最后倒数第二行,按回车的时候,浏览器崩溃。当时写文章的时候和现在修改都会出现这个 bug,很好重现,但是不知道在你们的电脑上会不会

Reply to helapu

这个问题,据说是 mac 输入法和 chrome 有兼容 bug,而且这个 bug 一年多了两边都不管。就这么挂着。简书的编辑器更严重,搞得每次编辑,都切换到 Safari 才行。

Reply to edwardzhou

原来如此 两边都不管 哈哈

You need to Sign in before reply, if you don't have an account, please Sign up first.