Erlang/OTP初学者指南:行为模式

32
据我从《Erlang和OTP实战》一书中所理解,单词“behavior”指的是以下内容:
- 行为接口,即一组功能函数; - 行为实现,即特定应用程序的代码(回调模块); - 行为容器,即一个进程。
问题:
1. Erlang/OTP初学者应该了解什么是行为?可以简单描述并理解OTP行为的概念吗? 2. 在Elang/OTP的上下文中,“回调函数”实际上是什么意思? 3. 我们能否将行为实现中的回调函数视为Java中重写方法? 4. 该书指出,在以下代码中与库函数“gen_server:start_link/4”相关联的回调函数是“Module:init/1”。
```` gen_server:start_link(SupFlags, Debug, Name, Mod, Args) -> Result ````
这是不是说明我们可以使用init/1调用gen_server:start_link/4库函数?还是说还有其他含义?
-module(tr_server).

-behaviour(gen_server).

-include_lib("eunit/include/eunit.hrl").

%% API
-export([
         start_link/1,
         start_link/0,
         get_count/0,
         stop/0
         ]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-define(SERVER, ?MODULE).
-define(DEFAULT_PORT, 1055).

-record(state, {port, lsock, request_count = 0}).


%%%===================================================================
%%% API
%%%===================================================================


%%--------------------------------------------------------------------
%% @doc Starts the server.
%%
%% @spec start_link(Port::integer()) -> {ok, Pid}
%% where
%%  Pid = pid()
%% @end
%%--------------------------------------------------------------------
start_link(Port) ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).

%% @spec start_link() -> {ok, Pid}
%% @doc Calls `start_link(Port)' using the default port.
s    tart_link() ->
    start_link(?DEFAULT_PORT).

%%--------------------------------------------------------------------
%% @doc Fetches the number of requests made to this server.
%% @spec get_count() -> {ok, Count}
%% where
%%  Count = integer()
%% @end
%%--------------------------------------------------------------------
get_count() ->
    gen_server:call(?SERVER, get_count).

%%--------------------------------------------------------------------
%% @doc Stops the server.
%% @spec stop() -> ok
%% @end
%%--------------------------------------------------------------------
stop() ->
    gen_server:cast(?SERVER, stop).


%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([Port]) ->
    {ok, LSock} = gen_tcp:listen(Port, [{active, true}]),
    {ok, #state{port = Port, lsock = LSock}, 0}.

handle_call(get_count, _From, State) ->
    {reply, {ok, State#state.request_count}, State}.

handle_cast(stop, State) ->
    {stop, normal, State}.

handle_info({tcp, Socket, RawData}, State) ->
    do_rpc(Socket, RawData),
    RequestCount = State#state.request_count,
    {noreply, State#state{request_count = RequestCount + 1}};
handle_info(timeout, #state{lsock = LSock} = State) ->
    {ok, _Sock} = gen_tcp:accept(LSock),
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

do_rpc(Socket, RawData) ->
    try
        {M, F, A} = split_out_mfa(RawData),
        Result = apply(M, F, A),
        gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Result]))
    catch
        _Class:Err ->
            gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Err]))
    end.

split_out_mfa(RawData) ->
    MFA = re:replace(RawData, "\r\n$", "", [{return, list}]),
    {match, [M, F, A]} =
        re:run(MFA,
               "(.*):(.*)\s*\\((.*)\s*\\)\s*.\s*$",
                   [{capture, [1,2,3], list}, ungreedy]),
    {list_to_atom(M), list_to_atom(F), args_to_terms(A)}.

args_to_terms(RawArgs) ->
    {ok, Toks, _Line} = erl_scan:string("[" ++ RawArgs ++ "]. ", 1),
    {ok, Args} = erl_parse:parse_term(Toks),
    Args.


%% test

start_test() ->
    {ok, _} = tr_server:start_link(1055).
5个回答

23

其他答案已经回答了您的具体问题,我将尝试用简单的语言解释行为背后的基础知识,并让您基于对这些基础知识的理解自己回答问题。

行为基本上是一个消息处理框架,在“框架”一词中,我指的是可以由最终用户完成和自定义的问题的部分解决方案。OTP行为基本上提供以下内容:

  • 一个消息循环
  • 与底层OTP支持的代码升级、跟踪、系统消息等集成。

行为将消息处理委托给回调模块,或者作为“Erlang and OTP In Action”中所称的行为实现。在调用其init/1函数时,回调模块通常为消息循环创建状态并代表其保留。然后,行为循环将该状态传递给每个后续调用的回调模块消息处理函数,并且这些调用中的每个调用都可以返回修改后的状态。回调函数还返回指令,告诉行为消息循环接下来要做什么。

以下是行为核心部分消息循环的大大简化版本:

loop(Callbacks, State) ->
  {Next, NState} =
 receive
                     M1 ->

                       Callbacks:handle_m1(M1,State);
                     M2 ->
                       Callbacks:handle_m2(M2,State);
                     Other ->
                       Callbacks:handle_other(Other,State)
                   end,
  case Next of

    stop -> ok;
    _ -> loop(Callbacks, NState)
  end.
该尾递归循环将Callbacks模块和State变量作为参数。在调用此循环之前,您已经告诉了行为(behavior)您的回调模块是什么,然后基本OTP行为支持代码已经调用了您的init/1回调函数以获取State的初始值。
我们的示例行为循环接收形式为M1、M2和任何其他消息的消息,这里不重要的细节,并且对于每个消息,在Callbacks模块中调用不同的回调函数。在此示例中,handle_m1和handle_m2回调函数分别处理消息M1和M2,而回调handle_other处理所有其他类型的消息。请注意,State被传递给每个回调函数。每个函数都应返回一个元组,其中第一个元素告诉循环下一步该做什么,第二个元素包含循环可能的新状态,即与State相同或不同的新值,循环将该新状态存储在其变量NState中。在此示例中,如果Next是原子stop,则循环停止,但如果是其他任何内容,则循环会递归地调用自身,并将新状态NState传递到下一次迭代中。由于它是尾递归,因此该循环永远不会超出堆栈。
如果您深入研究标准OTP行为(例如gen_server和gen_fsm)的源代码,您会发现一个很像这样的循环,但由于处理系统消息、超时、跟踪、异常等,它们更加复杂。标准行为还在单独的进程中启动它们的循环,因此它们还包含启动循环进程并向其传递消息的代码。

谢谢Steve,我会在继续阅读这本书的过程中参考你的答案。 - Kirill
1
终于我明白了行为是如何工作的。谢谢Steve! - jmracek

21

Q: 初学Erlang/OTP的人应该了解什么是behaviours?能否简要描述和理解OTP behaviours的概念?

Behaviour通常在代码中使用,以便编译器根据其行为生成更直观的错误消息,例如application/supervisor/gen_server/gen_event/gen_fsm。

它使编译器能够针对行为提供特定的错误消息,例如gen_server。

Q: 在Elang/OTP的上下文中,“回调函数”是什么意思?

回调函数可以说是从GUI编程(至少类似)中取出的。每当事件发生时,例如鼠标单击,就会有一个单独的函数处理鼠标单击。

因此,每当例如来自另一个模块的gen_server导出函数被调用时,该函数可以具有不同模式的回调函数(handle_call/handle_cast)。

Q: 我们可以将行为实现中的回调视为Java中覆盖的方法吗?

嗯...可能...也可能不是 :)

Q: 书中说以下代码中库函数“gen_server:start_link/4”的关联回调函数是“Module:init/1”。

gen_server:start_link本身调用init函数,如w55...(抱歉这是一个很长的名字)所回答的那样。


希望我已经回答了您的所有问题 :)


1
阿伦,这正是我一直在寻找的。 - Kirill

12

Erlang/OTP初学者应该了解哪些关于behaviours的知识?

可能可以参考这里的内容。

简单概括一下OTP behaviour的概念,是否可行?

从文档中可以得知:“Behaviours是这些常见模式的形式化。其想法是将一个进程的代码分为通用部分(behaviour模块)和特定部分(callback模块)。”

在Erlang/OTP上下文中,“回调函数”实际上是什么意思?

可以查看上面链接中提供的回调函数示例。

我们可以将behaviour实现中的回调函数视为Java中被覆盖的方法吗?

用Java术语来说,behaviour可能是Java接口,而callback则是在接口中定义的某个方法的实现。

在下面的代码中,书中说与库函数 'gen_server:start_link / 4' 相关的回调函数是 'Module:init / 1’,那么这是否意味着使用init / 1会调用gen_server:start_link/4库函数?还是意味着其他什么意思?

这意味着每次调用gen_server:start_link/4时,都会调用函数Module:init/1,其中Module是传递给start_link函数的第二个参数,参数则为你提供的第四个参数。换句话说,这是start_link / 4背后所发生的事情:

...
start_link(Name, Module, Args, Opts) ->
  ...
  Module:init(Args)
  ...
...

我认为此答案并不是很有帮助。其中很多内容都像是 RTFM,这种暗示可能会让人感到自己没有阅读过相关文档,但实际上却与问题无关。对于学习来说,提供概念的替代解释通常更易懂。在 StackOverflow 上,用户应该努力提供自己的答案,而不是仅仅提供指针,除非明确要求提供指针。 - mwt
服务器的代码可以重写为通用部分server.erl:..你能解释一下这句话吗?我以为整个代码都是为服务器而写的。 - HIRA THAKUR

3

请查看您的Erlang lib目录中gen_server模块的源代码。 源代码非常详细地解释了此模块,注释也非常详尽。


0

gen_server:start_link 调用 init。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接