事件處理器

處理這個!*上膛散彈槍*

在之前的幾個範例中,我刻意避開了一些東西。如果您回顧一下提醒應用程式,您會發現我提到過我們可以通知客戶端,無論他們是 IM、郵件等。在前一章中,我們的交易系統使用 io:format/2 來通知人們正在發生的事情。

您可能可以看到這兩種情況之間的共同點。它們都是關於讓人員(或某些進程或應用程式)知道在某個時間點發生的事件。在一個例子中,我們只輸出結果,而在另一個例子中,我們在發送訊息之前取得訂閱者的 Pid。

輸出方法是簡約的,並且無法輕鬆擴展。使用訂閱者的方法當然是有效的。事實上,當每個訂閱者在收到事件後都有一個長時間運行的操作要執行時,它非常有用。在更簡單的情況下,您不一定希望有一個待命的進程等待每個回呼的事件,則可以採用第三種方法。

第三種方法只是接收一個接受函數的進程,並讓它們在任何傳入的事件上執行。這個進程通常稱為事件管理器,它可能看起來有點像這樣

Shows a bubble labeled 'your server', another one labeled 'event manager'. An arrow (representing an event) goes from your server to the event manager, which has the event running in callback functions (illustrated as f, y and g)

這樣做有一些優點

當然也有一些缺點

有一種方法可以解決這些缺點,這有點令人失望。基本上,您必須將事件管理器方法轉換為訂閱者方法。幸運的是,事件管理器方法足夠靈活,可以輕鬆地做到這一點,我們將在本章稍後看到如何做到。

我通常會事先編寫一個非常基本的 OTP 行為版本,我們將在純 Erlang 中看到,但在這種情況下,我將直接切入重點。gen_event 來了。

通用事件處理器

gen_event 行為與 gen_servergen_fsm 行為有很大的不同,因為您永遠不會真正啟動一個進程。我上面描述的關於「接受回呼」的整個部分是原因。gen_event 行為基本上運行接受並呼叫函數的進程,而您只需要提供一個包含這些函數的模組。這意味著,除了以事件管理器喜歡的格式提供回呼函數外,您無需處理任何事件操作。所有管理都是免費完成的;您只需提供特定於您的應用程式的內容。這並不令人意外,因為 OTP 再次是關於將通用與特定分開的。

然而,這種分離意味著標準的 spawn -> init -> loop -> terminate 模式只會應用於事件處理器。現在,如果您回憶一下之前所說的,事件處理器是一組在管理器中運行的函數。這表示目前呈現的模型

common process pattern: spawn -> init -> loop -> exit

對程式設計師來說,會變成更像這樣

 spawn event manager -> attach handler -> init handler -> loop (with handler calls) -> exit handlers

每個事件處理器都可以保存自己的狀態,由管理器為它們攜帶。然後每個事件處理器都可以採用這種形式

init --> handle events + special messages --> terminate

這不是太複雜,所以讓我們開始處理事件處理器的回呼本身。

init 和 terminate

init 和 terminate 函數與我們在之前伺服器和有限狀態機的行為中看到的情況類似。 init/1 接受一個參數列表並返回 {ok, State}init/1 中發生的任何事情都應該在 terminate/2 中有對應的內容。

handle_event

handle_event(Event, State) 函數或多或少是 gen_event 回呼模組的核心。它的工作方式類似於 gen_serverhandle_cast/2,它是以非同步方式工作的。它與它可以返回的內容不同

A sleeping polar bear with a birthday hat

元組 {ok, NewState} 的工作方式與我們在 gen_server:handle_cast/2 中看到的方式類似;它只是更新自己的狀態,而不回覆任何人。在 {ok, NewState, hibernate} 的情況下,值得注意的是,整個事件管理器都將被置於休眠狀態。請記住,事件處理器與它們的管理器在同一個進程中運行。然後 remove_handler 從管理器中刪除處理器。當您的事件處理器知道它已完成並且沒有其他事情要做時,這可能會很有用。最後,還有 {swap_handler, Args1, NewState, NewHandler, Args2}。這個不常用,但它的作用是刪除目前的事件處理器並用新的處理器替換它。這是通過首先呼叫 CurrentHandler:terminate(Args1, NewState) 並刪除目前的處理器,然後通過呼叫 NewHandler:init(Args2, ResultFromTerminate) 來新增一個新的處理器來完成的。在您知道發生某些特定事件並且最好將控制權交給新的處理器的情況下,這會很有用。這很可能是您在需要時會知道的那種情況。同樣,它並不常用。

所有傳入的事件都可能來自 gen_event:notify/2,它與 gen_server:cast/2 一樣是非同步的。還有 gen_event:sync_notify/2,它是同步的。說起來有點好笑,因為 handle_event/2 仍然是非同步的。這裡的想法是,函數呼叫僅在所有事件處理器都看到並處理了新訊息後才會返回。在此之前,事件管理器將通過不回覆來阻止呼叫進程。

handle_call

這類似於 gen_serverhandle_call 回呼,只是它可以返回 {ok, Reply, NewState}{ok, Reply, NewState, hibernate}{remove_handler, Reply}{swap_handler, Reply, Args1, NewState, Handler2, Args2}gen_event:call/3-4 函數用於進行呼叫。

這引發了一個問題。當我們有 15 個不同的事件處理器時,這是如何工作的?我們是期望 15 個回覆,還是只期望一個包含所有回覆的回覆?事實上,我們將被迫選擇只有一個處理器回覆我們。當我們真正看到如何將處理器附加到我們的事件管理器時,我們將深入了解如何完成此操作的詳細資訊,但是如果您不耐煩,您可以查看函數 gen_event:add_handler/3 的工作方式,以嘗試弄清楚。

handle_info

handle_info/2 回呼與 handle_event 非常相似(相同的返回值和所有內容),唯一的例外是它只處理帶外訊息,例如退出訊號、使用 ! 運算子直接傳送到事件管理器的訊息等。它的用例與 gen_servergen_fsm 中的 handle_info 類似。

code_change

程式碼變更的工作方式與 gen_server 的工作方式完全相同,只是它是針對每個單獨的事件處理器。它接受 3 個參數 OldVsnStateExtra,它們依次是版本號、目前處理器的狀態和我們可以忽略的數據。它只需要返回 {ok, NewState}

是時候玩冰壺了!

有了看到的回呼,我們可以開始研究使用 gen_event 實作一些東西。在本章的這部分中,我選擇建立一組事件處理器,用於追蹤世界上最有趣的運動之一:冰壺的比賽更新。

如果您以前從未看過或玩過冰壺(這很可惜!),規則相對簡單

A top view of a curling ice/game

您有兩支隊伍,他們嘗試將冰壺石滑到冰上,試圖到達紅色圓圈的中心。他們用 16 個石頭執行此操作,並且在回合結束時(稱為),最接近中心的石頭的隊伍會贏得一分。如果該隊伍擁有兩個最接近的石頭,則它會獲得兩分,依此類推。共有 10 局,並且在 10 局結束時得分最高的隊伍贏得比賽。

還有更多規則讓比賽更加精彩,但這是一本關於 Erlang 的書,而不是關於非常精彩的冬季運動的書。如果您想了解更多關於規則的資訊,我建議您前往維基百科上關於冰壺的文章

對於這個完全與現實世界相關的場景,我們將為下一屆冬季奧運會工作。所有事情發生的城市剛完成建造比賽場地,他們正在準備記分板。事實證明,我們必須編寫一個系統,讓一些官員輸入比賽事件,例如何時擲出石頭、何時回合結束或何時比賽結束,然後將這些事件路由到記分板、統計系統、新聞記者資訊饋送等。

我們很聰明,我們知道這是關於 gen_event 的章節,並推斷我們可能會使用它來完成我們的任務。由於這更像是一個範例,我們不會實作所有規則,但是當我們完成本章後,您可以隨意實作。我保證不會生氣。

我們將從記分板開始。由於他們現在正在安裝它,我們將使用一個虛擬模組,該模組通常會讓我們與其互動,但目前它只會使用標準輸出顯示正在發生的事情。這就是 curling_scoreboard_hw.erl 的用武之地

-module(curling_scoreboard_hw).
-export([add_point/1, next_round/0, set_teams/2, reset_board/0]).

%% This is a 'dumb' module that's only there to replace what a real hardware
%% controller would likely do. The real hardware controller would likely hold
%% some state and make sure everything works right, but this one doesn't mind.

%% Shows the teams on the scoreboard.
set_teams(TeamA, TeamB) ->
    io:format("Scoreboard: Team ~s vs. Team ~s~n", [TeamA, TeamB]).

next_round() ->
    io:format("Scoreboard: round over~n").

add_point(Team) ->
    io:format("Scoreboard: increased score of team ~s by 1~n", [Team]).

reset_board() ->
    io:format("Scoreboard: All teams are undefined and all scores are 0~n").

這就是計分板的全部功能。它們通常會有計時器和其他很棒的功能,但管他的。奧運委員會似乎不希望我們為了教學而實作這些瑣碎的功能。

這個硬體介面讓我們可以有一些設計時間。我們知道目前有幾個事件需要處理:新增隊伍、進入下一輪、設定分數。我們只會在開始新遊戲時使用 reset_board 功能,而不會將其作為協定的一部分。我們需要的事件在我們的協定中可能採用以下形式:

我們可以從這個基本的事件處理器骨架開始實作:

-module(curling_scoreboard).
-behaviour(gen_event).

-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
   terminate/2]).

init([]) ->
    {ok, []}.

handle_event(_, State) ->
    {ok, State}.

handle_call(_, State) ->
    {ok, ok, State}.

handle_info(_, State) ->
    {ok, State}.

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

terminate(_Reason, _State) ->
    ok.

這是我們可以為每個 gen_event 回呼模組使用的骨架。目前,計分板事件處理器本身不需要做任何特殊的事情,只需將呼叫轉發到硬體模組即可。我們期望事件來自 gen_event:notify/2,因此協定的處理應該在 handle_event/2 中完成。檔案 curling_scoreboard.erl 顯示了更新。

-module(curling_scoreboard).
-behaviour(gen_event).

-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
   terminate/2]).

init([]) ->
    {ok, []}.

handle_event({set_teams, TeamA, TeamB}, State) ->
    curling_scoreboard_hw:set_teams(TeamA, TeamB),
    {ok, State};
handle_event({add_points, Team, N}, State) ->
    [curling_scoreboard_hw:add_point(Team) || _ <- lists:seq(1,N)],
    {ok, State};
handle_event(next_round, State) ->
    curling_scoreboard_hw:next_round(),
    {ok, State};
handle_event(_, State) ->
    {ok, State}.

handle_call(_, State) ->
    {ok, ok, State}.

handle_info(_, State) ->
    {ok, State}.

你可以看到對 handle_event/2 函式所做的更新。試試看:

1> c(curling_scoreboard_hw).
{ok,curling_scoreboard_hw}
2> c(curling_scoreboard).
{ok,curling_scoreboard}
3> {ok, Pid} = gen_event:start_link().
{ok,<0.43.0>}
4> gen_event:add_handler(Pid, curling_scoreboard, []).
ok
5> gen_event:notify(Pid, {set_teams, "Pirates", "Scotsmen"}).
Scoreboard: Team Pirates vs. Team Scotsmen
ok
6> gen_event:notify(Pid, {add_points, "Pirates", 3}). 
ok
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
7> gen_event:notify(Pid, next_round). 
Scoreboard: round over
ok
8> gen_event:delete_handler(Pid, curling_scoreboard, turn_off).
ok
9> gen_event:notify(Pid, next_round). 
ok

這裡發生了一些事情。首先,我們將 gen_event 程序作為獨立的實體啟動。然後,我們使用 gen_event:add_handler/3 動態地將我們的事件處理器附加到它上面。你可以根據需要多次執行此操作。然而,如先前在 handle_call 部分所述,當你想要使用特定的事件處理器時,這可能會導致問題。如果你想在有多個事件處理器實例時呼叫、新增或刪除特定的處理器,你必須找到一種方法來唯一識別它。我最喜歡的方法(如果你沒有更具體的想法,這是一個很棒的方法)是直接使用 make_ref() 作為唯一值。要將此值給予處理器,你只需呼叫 add_handler/3 並將其寫成 gen_event:add_handler(Pid, {Module, Ref}, Args)。從此時起,你可以使用 {Module, Ref} 與該特定處理器進行通訊。問題解決了。

A curling stone

無論如何,你可以看到我們將訊息傳送到事件處理器,它成功地呼叫了硬體模組。然後我們刪除處理器。這裡的 turn_offterminate/2 函式的一個參數,我們目前的實作並不關心它。處理器已消失,但我們仍然可以將事件傳送到事件管理器。萬歲!

上面程式碼片段中一個令人尷尬的事情是,我們被迫直接呼叫 gen_event 模組,並向所有人展示我們的協定是什麼樣子。一個更好的選擇是在其之上提供一個 抽象模組,該模組只會封裝我們需要的一切。這對於使用我們程式碼的每個人來說會看起來更棒,並且可以讓我們在需要時(或當需要時)更改實作。它還可以讓我們指定標準冰壺比賽需要包含哪些處理器。

-module(curling).
-export([start_link/2, set_teams/3, add_points/3, next_round/1]).

start_link(TeamA, TeamB) ->
    {ok, Pid} = gen_event:start_link(),
    %% The scoreboard will always be there
    gen_event:add_handler(Pid, curling_scoreboard, []),
    set_teams(Pid, TeamA, TeamB),
    {ok, Pid}.

set_teams(Pid, TeamA, TeamB) ->
    gen_event:notify(Pid, {set_teams, TeamA, TeamB}).

add_points(Pid, Team, N) ->
    gen_event:notify(Pid, {add_points, Team, N}).

next_round(Pid) ->
    gen_event:notify(Pid, next_round).

現在執行它:

1> c(curling).
{ok,curling}
2> {ok, Pid} = curling:start_link("Pirates", "Scotsmen").
Scoreboard: Team Pirates vs. Team Scotsmen
{ok,<0.78.0>}
3> curling:add_points(Pid, "Scotsmen", 2). 
Scoreboard: increased score of team Scotsmen by 1
Scoreboard: increased score of team Scotsmen by 1
ok
4> curling:next_round(Pid). 
Scoreboard: round over
ok
Some kind of weird looking alien sitting on a toilet, surprised at the newspapers it is reading

這看起來似乎沒有什麼優勢,但實際上是為了讓程式碼使用起來更方便(並減少寫錯訊息的可能性)。

通知媒體!

我們已經完成了基本的計分板,現在我們希望國際記者能夠從負責更新我們系統的官方人員那裡取得即時數據。因為這是一個範例程式,我們不會逐步設定 socket 並編寫更新協定,但我們會建立系統,方法是讓一個中介程序負責處理它。

基本上,每當新聞機構想要加入比賽 feed 時,他們都會註冊自己的處理器,該處理器只會將他們需要的數據轉發給他們。我們實際上要把我們的 gen_event 伺服器變成某種訊息樞紐,只是將它們路由到需要它們的任何人。

首先要做的是使用新的介面更新 curling.erl 模組。因為我們希望事情易於使用,我們只會新增兩個函式,join_feed/2leave_feed/2。加入 feed 應該只需要輸入事件管理器的正確 Pid 和轉發所有事件的 Pid 即可。這應該會返回一個唯一值,然後可以使用該值透過 leave_feed/2 取消訂閱 feed。

%% Subscribes the pid ToPid to the event feed.
%% The specific event handler for the newsfeed is
%% returned in case someone wants to leave
join_feed(Pid, ToPid) ->
    HandlerId = {curling_feed, make_ref()},
    gen_event:add_handler(Pid, HandlerId, [ToPid]),
    HandlerId.

leave_feed(Pid, HandlerId) ->
    gen_event:delete_handler(Pid, HandlerId, leave_feed).

請注意,我正在使用先前描述的用於多個處理器的技術({curling_feed, make_ref()})。你可以看到此函式預期一個名為 curling_feed 的 gen_event 回呼模組。如果我只使用模組的名稱作為 HandlerId,事情仍然會正常運作,只是當我們完成一個實例時,我們無法控制要刪除哪個處理器。事件管理器只會以未定義的方式選擇其中一個。使用 Ref 可確保來自 Head-Smashed-In Buffalo Jump 新聞界的某人離開該地時,不會斷開《經濟學人》記者的連線(不知道為什麼他們要對冰壺進行報導,但誰知道呢)。無論如何,這是我建立的 curling_feed 模組的實作:

-module(curling_feed).
-behaviour(gen_event).

-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
   terminate/2]).

init([Pid]) ->
    {ok, Pid}.

handle_event(Event, Pid) ->
    Pid ! {curling_feed, Event},
    {ok, Pid}.

handle_call(_, State) ->
    {ok, ok, State}.

handle_info(_, State) ->
    {ok, State}.

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

terminate(_Reason, _State) ->
    ok.

這裡唯一有趣的事情仍然是 handle_event/2 函式,它會盲目地將所有事件轉發到訂閱的 Pid。現在當我們使用新的模組時:

1> c(curling), c(curling_feed).
{ok,curling_feed}
2> {ok, Pid} = curling:start_link("Saskatchewan Roughriders", "Ottawa Roughriders").
Scoreboard: Team Saskatchewan Roughriders vs. Team Ottawa Roughriders
{ok,<0.165.0>}
3> HandlerId = curling:join_feed(Pid, self()). 
{curling_feed,#Ref<0.0.0.909>}
4> curling:add_points(Pid, "Saskatchewan Roughriders", 2). 
Scoreboard: increased score of team Saskatchewan Roughriders by 1
ok
Scoreboard: increased score of team Saskatchewan Roughriders by 1
5> flush().
Shell got {curling_feed,{add_points,"Saskatchewan Roughriders",2}}
ok
6> curling:leave_feed(Pid, HandlerId).
ok
7> curling:next_round(Pid). 
Scoreboard: round over
ok
8> flush().
ok

我們可以看見我們將自己加入 feed、收到更新,然後離開並停止接收它們。你實際上可以嘗試多次新增許多程序,它也會正常運作。

但是,這會產生一個問題。如果其中一個冰壺 feed 訂閱者當機怎麼辦?我們是否應該讓處理器繼續執行?理想情況下,我們不應該這樣做。事實上,我們不必這樣做。所有需要做的是將呼叫從 gen_event:add_handler/3 更改為 gen_event:add_sup_handler/3。如果你當機,處理器就會消失。然後,在另一端,如果 gen_event 管理器當機,訊息 {gen_event_EXIT, Handler, Reason} 會被送回給你,以便你可以處理它。夠簡單吧?再想想。

不要喝太多「酷愛」飲料

alien kid on a leash

你可能在童年的時候,會去阿姨或祖母家參加派對之類的事情。如果你以任何方式惡作劇,除了你的父母之外,還會有好幾個大人看著你。現在,如果你做錯了任何事情,你的媽媽、爸爸、阿姨、祖母都會責罵你,然後即使你已經清楚知道自己做錯了,每個人還是會一直告訴你。嗯,gen_event:add_sup_handler/3 有點像那樣;不,我是認真的。

每當你使用 gen_event:add_sup_handler/3 時,你的程序和事件管理器之間會建立一個連結,以便它們都被監管,並且處理器知道它的父程序是否失敗。如果你記得 錯誤和程序 章節及其關於監視器的部分,我提到監視器 對於編寫需要知道其他程序發生什麼事的程式庫很有用,因為它們可以堆疊,與連結相反。嗯,gen_event 早於監視器在 Erlang 中出現,並且對向後相容性的強烈承諾導致了這個非常糟糕的缺陷。基本上,因為你可以讓同一個程序充當許多事件處理器的父程序,所以程式庫永遠不會取消連結這些程序(除非它永遠終止),以防萬一。監視器實際上可以解決這個問題,但它們沒有在那裡被使用。

這表示當你自己的程序當機時,一切都會順利進行:受監管的處理器會終止(透過呼叫 YourModule:terminate({stop, Reason}, State))。當你的處理器本身當機時(但不是事件管理器),一切也會順利進行:你會收到 {gen_event_EXIT, HandlerId, Reason}。但是,當事件管理器關閉時,你會收到以下其中之一:

這是一個相當大的缺陷,但至少你已經知道了。你可以嘗試將你的事件處理器切換為受監管的事件處理器,如果你覺得想這麼做。即使在某些情況下可能會更煩人,但它也會更安全。安全第一。

我們還沒完成!如果某些媒體成員沒有準時到達怎麼辦?我們需要能夠從 feed 中告訴他們目前比賽的狀況。為此,我們將編寫一個名為 curling_accumulator 的額外事件處理器。同樣地,在完全編寫它之前,我們可能想要將它新增到 curling 模組中,並包含我們想要的一些呼叫:

-module(curling).
-export([start_link/2, set_teams/3, add_points/3, next_round/1]).
-export([join_feed/2, leave_feed/2]).
-export([game_info/1]).

start_link(TeamA, TeamB) ->
    {ok, Pid} = gen_event:start_link(),
    %% The scoreboard will always be there
    gen_event:add_handler(Pid, curling_scoreboard, []),
    %% Start the stats accumulator
    gen_event:add_handler(Pid, curling_accumulator, []),
    set_teams(Pid, TeamA, TeamB),
    {ok, Pid}.

%% skipping code here

%% Returns the current game state.
game_info(Pid) ->
    gen_event:call(Pid, curling_accumulator, game_data).

這裡需要注意的是,game_info/1 函式只使用 curling_accumulator 作為處理器 id。如果你有多個相同處理器的版本,關於使用 make_ref()(或任何其他方法)以確保你寫入正確處理器的提示仍然有效。另請注意,我讓 curling_accumulator 處理器自動啟動,就像計分板一樣。現在來看模組本身。它應該能夠保存冰壺比賽的狀態:到目前為止,我們需要追蹤的隊伍、分數和輪數。這一切都可以保存在狀態記錄中,並在每次收到事件時變更。然後,我們只需要回覆 game_data 呼叫,如下所示:

-module(curling_accumulator).
-behaviour(gen_event).

-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
   terminate/2]).

-record(state, {teams=orddict:new(), round=0}).

init([]) ->
    {ok, #state{}}.

handle_event({set_teams, TeamA, TeamB}, S=#state{teams=T}) ->
    Teams = orddict:store(TeamA, 0, orddict:store(TeamB, 0, T)),
    {ok, S#state{teams=Teams}};
handle_event({add_points, Team, N}, S=#state{teams=T}) ->
    Teams = orddict:update_counter(Team, N, T),
    {ok, S#state{teams=Teams}};
handle_event(next_round, S=#state{}) ->
    {ok, S#state{round = S#state.round+1}};
handle_event(_Event, Pid) ->
    {ok, Pid}.

handle_call(game_data, S=#state{teams=T, round=R}) ->
    {ok, {orddict:to_list(T), {round, R}}, S};
handle_call(_, State) ->
    {ok, ok, State}.

handle_info(_, State) ->
    {ok, State}.

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

terminate(_Reason, _State) ->
    ok.

所以你可以看到我們基本上只是更新狀態,直到有人要求比賽詳細資料,屆時我們將把它們傳送回去。我們以非常基本的方式完成了這項工作。一種可能更聰明的方式來組織程式碼是,簡單地保留遊戲中曾經發生過的所有事件的列表,這樣我們每次有新的程序訂閱 feed 時,就可以將它們一次全部傳送回去。這裡不需要這樣做來展示事情是如何運作的,所以讓我們專注於使用我們的新程式碼:

1> c(curling), c(curling_accumulator).
{ok,curling_accumulator}
2> {ok, Pid} = curling:start_link("Pigeons", "Eagles").
Scoreboard: Team Pigeons vs. Team Eagles
{ok,<0.242.0>}
3> curling:add_points(Pid, "Pigeons", 2).
Scoreboard: increased score of team Pigeons by 1
ok
Scoreboard: increased score of team Pigeons by 1
4> curling:next_round(Pid).
Scoreboard: round over
ok
5> curling:add_points(Pid, "Eagles", 3).
Scoreboard: increased score of team Eagles by 1
ok
Scoreboard: increased score of team Eagles by 1
Scoreboard: increased score of team Eagles by 1
6> curling:next_round(Pid).
Scoreboard: round over
ok
7> curling:game_info(Pid).
{[{"Eagles",3},{"Pigeons",2}],{round,2}}

引人入勝!奧運委員會一定會喜歡我們的程式碼。我們可以自我表揚、兌現巨額支票,然後整晚玩電子遊戲了。

我們還沒有看到作為一個模組的 gen_event 的全部功能。事實上,我們還沒有看到事件處理器最常見的用途:記錄和系統警報。我決定不展示它們,因為幾乎所有其他關於 Erlang 的資源都嚴格地將 gen_event 用於此目的。如果你有興趣進入這裡,請先查看 error_logger

即使我們還沒有看到 gen_event 最常見的用法,但重要的是要說明我們已經學過理解它們、建構自己的 gen_event 以及將它們整合到應用程式中所需的所有概念。更重要的是,我們終於涵蓋了在實際程式碼開發中使用的三個主要的 OTP 行為。我們還有一些行為需要學習——那些充當我們所有工作進程之間黏合劑的行為——例如監管者。