19. イベントハンドラ

19.1. これを使え! *ショットガンをパンプしろ*

これまで取り上げたいくつかの例の中で、深入りするのを避けてきたことがあります。 リマインダーアプリケーションを振り返ってみると、私が、クライアントがIM、メール、その他であろうと、通知できると言ったのを見つけられるでしょう。 また前の章では、私たちの取引システムは何が起きているかをユーザに通知するために io:format/2 を使いました。

これら2つの例で共通点を見つけられることでしょう。 これらはともに人々(あるいはあるプロセスやアプリケーション)にある瞬間にイベントが起きたことを知らせる、というものです。 一方では結果を出力するだけ、また一方ではメッセージを送る前にサブスクライバのPidを取得します。

出力するという手段は必要最小限の方法で、拡張は用意ではありません。 サブスクライバをとる方法が確実に妥当です。 事実、各サブスクライバがイベントを受け取った後に長時間動作する操作があるときにかなり役立ちます。 もっと単純な場合、各コールバックのためのイベントを待つために待機プロセスを必ずしも持ちたいとは思わないときは、3つ目の方法を取ることができます。

この3つ目の方法は、単純に関数を受け取るプロセスを立て、入ってくるどのようなイベントに対してもその関数を走らせるというものです。 このプロセスは通常イベントマネージャと呼ばれ、たいていこのような形で落ち着くでしょう:

../_images/event-manager.png

この方法での利点としては:

  • サーバのサブスクライバがたくさんあった場合でも、イベントを一度転送する必要しかないので稼働し続けられる
  • 転送されるデータがたくさんあった場合、転送は1度だけで済み、すべてのコールバックはそのデータの同じインスタンスに対して操作を行います
  • 短命なタスクに対して新しいプロセスを生成する必要がありません

もちろん、不都合な点もあります:

  • すべての関数が長時間動作する必要があった場合、お互いにブロックしあいます。 これは実際にはイベントをプロセスに転送する関数を持ち、基本的にはイベントマネージャをイベントフォワーダ(リマインダーアプリケーションでやったのと似たようなこと)にすれば防げます
  • 事実、無限ループをする関数は何かがクラッシュするまで新しいイベントが処理されることを妨げます

いささか面白くない方法ではありますが、これらの欠点を解決する方法があります。 基本的には、イベントマネージャをサブスクライバのモデルに変更しなければいけません。 幸いにもイベントマネージャの手法は十分柔軟性があるので簡単にその対応ができます。この章で後ほどどのように行うか見ていきます。

通常は先に純粋にErlangで書いた非常に基本的なOTPビヘイビアを書くのですが、今回は、いきなり本題に入ります。 gen_event の登場です。

19.2. 汎用イベントハンドラ

gen_event ビヘイビアはプロセスを全く立ち上げていないという点で gen_servergen_fsm といったビヘイビアとはかなり異なります。 「コールバックを受け入れる」に関して上で説明した部分全体が、この違いの理由です。 gen_event ビヘイビアは基本的に、関数を受け入れ、呼び出すプロセスを実行し、開発者はこれらの関数をふくむモジュールを提供するだけとなります。 これはつまり、イベントマネージャの要求を満たすコールバック関数を与える以外のイベント操作は何もすることがないということです。 そのような管理は、こちらが何もせずともすべて行なってもらえるのです。 ただ、あなたのアプリケーション特有のものだけを提供するだけです。 これは、再度となりますが、OTP自体が要は特定の用途のものから汎用なものを切り離すことなので、なにも驚くことではありません。

しかしながら、このようなコードの分離は、標準の spawn -> init -> loop -> terminate のパターンしかイベントハンドラに適用できないということになります。 以前お伝えしたことを思い出してみると、イベントハンドラはマネージャの中で動いている関数の束でした。 これはつまり、現在提示されている次のようなモデルが:

../_images/common-pattern1.png

プログラマにとっては、次の図のように切り替わるということです:

../_images/event-handler-pattern.png

各イベントハンドラは各々の状態を保持していて、状態はマネージャによって持ち回されます。 各イベントハンドラは次のような形式を取ることができます:

../_images/event-handler-pattern-local.png

複雑すぎるものはなにもないので、 This is nothing too complex so let’s get it on with the event handlers’ callbacks themselves.

19.2.1. initとterminate

initとterminateの各関数はこれまで取り上げたサーバや有限状態マシンのビヘイビアにあったものと似ています。 init/1 は引数のリストを取って、 {ok, State} を返します。 init/1 で何が起きても、 terminate/2 の相方となります。

19.2.2. handle_event

handle_event(Event, State) 関数は多かれ少なかれ gen_event のコールバックモジュールの核となるものです。 これは非同期に動くという点で gen_serverhandle_cast/2 と似た動作をします。 しかし戻り値が異なります:

  • {ok, NewState}
  • {ok, NewState, hibernate} これはイベントマネージャ自体を次のイベントまでハイバネートさせます
  • remove_handler
  • {swap_handler, Args1, NewState, NewHandler, Args2}
../_images/hibernate.png

{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_event:cast/2 のように非同期です。 gen_event:sync_notify/2 という同期の関数もあります。 これはちょっと面白くて、 handle_event/2 は非同期なままなのです。 こうする理由は、関数呼び出しはすべてのイベントハンドラが新しいメッセージを確認して処理を行った後にだけ戻り値を返すからです。 それまでは、イベントマネージャは戻り値を返さないことで呼び出しているプロセスをブロックし続けます。

19.2.3. handle_call

これは gen_serverhandle_call コールバックと似ていますが、 {ok, Reply, NewState}, {ok, Reply, NewState, hibernate}, {remove_handler, Reply}, {swap_handler, Reply, Args1, NewState, Handle_Call} を返すという点で異なります。 gen_server:call/3-4 関数が呼び出しに使われます。

ここで疑問が出てきます。 15個の異なるイベントハンドラがあった場合、これはどう動作するのでしょうか。 戻り値が15個来るのでしょうか、それともすべての返り値を含むものが1つだけ返ってくるのでしょうか。 実際は、戻り値を返すハンドラを1つだけ選ばされます。 これがどのように行われるかの細かな部分については、私たちが実際にイベントマネージャへのハンドラ追加方法について見るときに深く見てきますが、気が短い人は、 gen_event:add_handler/3 関数がどのように動作するかを見ればわかるかと思います。

19.2.4. handle_info

handle_info/2 コールバックは handle_event とほとんど一緒です。(同じ戻り値で、すべてが一緒です)ただし、終了シグナルや ! 演算子で直接イベントマネージャに送られたメッセージなどのアウトオブバンドメッセージのみを扱う点だけが異なります。 gen_servergen_fsmhandle_info と似たユースケースで用いられます。

19.2.5. code_change

code_change は、個々のイベントハンドラに対してのものという点以外は、 gen_server にあったものとまったく同じ動作をします。 これは OldVsn, State, Extra という順番で引数を3つ取ります。それぞれ、バージョン番号、今あるハンドラの状態、今のところは無視できるデータ、が入っています。 戻り値として {ok, NewState} だけ返せば良いです。

19.3. カーリングの時間です!

コールバックについて見てきたので、 gen_event を使って何か実装してみましょう。 この章では、世界で最も面白いスポーツの速報を出すのに使うイベントハンドラ一式を作ることにしました。そのスポーツは、カーリングです。

もし、過去にカーリングを見たことが1度も無ければ(大変遺憾です!)、ルールは比較的単純なので覚えてください:

../_images/curling-ice.png

2つチームがあり、それぞれが氷上でカーリングストーンを送り、赤い丸の中心に置こうとします。 合計16個の石でこれを行い、もっとも円の中心に最も近いストーンを置いたチームが勝ちとなり、ラウンドの最後にポイントを獲得します。(エンドと呼ばれます) 最も近いストーンが2つあった場合は2ポイント獲得します。 10回のエンドを行い、最後のエンドが完了した時点で点数が多いチームが勝利となります。

ゲームを魅力的にするルールがもっとありますが、これはErlangの本で、極めて魅力的なウィンタースポーツの本ではありません。 もしルールについてもっと知りたければ、Wikipediaでカーリングの項目を調べることをおすすめします。

この完全に実世界に関連したシナリオで、次のオリンピックに向けて準備しましょう。 開催される年では試合が行われるアリーナがちょうど建設が完了して、スコアボードも設置完了しようとしています。 私たちがシステムをプログラムしなければならないことがわかりました。そのシステムで、職員が試合のイベントを入力します。たとえばストーンが投げられた時、ラウンドが終わった時、あるいは試合が終了した時、などです。そしてこれらのイベントをスコアボードや、統計システム、ニュースレポーターへの速報、などに送ります。

私たちは賢いので、この章は 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").

これがスコアボードにある機能すべてです。 スコアボードには通常タイマーや他の素晴らしい機能がついていますが、それはいまはどうでもいいです。 オリンピック委員会が私たちにチュートリアルのためにつまらない実装をされているなんて思うことはないでしょう。

このハードウェアインターフェースは私たちに設計を考える時間をくれます。 いまのところ、扱うイベントは2、3つしか無いことがわかっています。チームの追加、次のラウンドへの移行、ポイントの設定の3つです。 新しい試合を始めるときには reset_board 機能だけを使いますが、これはプロトコルの一部として設定する必要はないでしょう。 私たちのプロトコルではイベントは次のような形式である必要があるでしょう。

  • {set_teams, TeamA, TeamB} これは、 curling_scoreboard_hw:set_teams(TeamA, TeamB) の1回の呼び出しに変換されます
  • {add_points, Team, N} これは curling_scoreboard_hw:add_points(Team) のN回の呼び出しに変換されます
  • next_round これは同名の関数の1回の呼び出しに変換されます

この基本的なイベントハンドラのスケルトンを使って実装を開始できます:

-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_hw: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 の部分で触れたように、特定のイベントハンドラとやりとりしたい時に問題を起こしかねません。 ある特定のハンドラのインスタンスが1つ以上あるときに、それを呼び出し、追加、または削除する場合、一意にそれを見つける方法を決めなければいけません。 私のお気に入りの方法は(もしあなたが特にやり方を決めていないならおすすめです)、一意な値として make_ref() を使うだけ、というものです。 この値をハンドラに与えるために、 add_handler/3gen_event:add_handler(Pid, {Module, Ref}, Args) の形で呼び出します。 このときから、 {Module, Ref} を使ってその特定のハンドラに話しかけることができます。 問題解決です。

../_images/curling-stone.png

とにかく、私たちがイベントハンドラにメッセージを送っているところを確認できるでしょう。そして、それが無事ハードウェアモジュールを呼び出していることでしょう。 ここで、 turn_offterminate/2 関数の引数ですが、今の実装では気にする必要はありません。 ハンドラはなくなってしまいましたが、イベントマネージャにはまだイベントを送ることができます。 やったね。

上のコード片で1点おかしなところは、 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
../_images/alert.png

それほど大きな利点があるようには見えませんが、本当にこれでコードが使いやすくなるんです。(そしてメッセージを間違って書いてしまう可能性を下げます)

19.4. プレスに警告しろ!

基本的なスコアボードは実装が終わったので、私たちのシステムを更新する責任がある職員から、国際レポーターが速報を取得できるようにしたいですね。 これはサンプルプログラムなので、ソケットを設定する手順や更新用のプロトコルを書くことに関して細かく見ていきませんが、それらの責任を持つ中間プロセスを置くことで、システムはすべて準備万端とします。

基本的に、報道機関が試合の速報フィードを取得したいと思ったときはいつでも、彼らは必要なデータを転送するだけの、独自のハンドラを登録します。 私たちは効率的に gen_event サーバをメッセージハブのようなものに変更し、メッセージを必要な人には誰にでも転送だけするようにします。

まず最初にやることは、 curling.erl モジュールを更新して新しいインターフェイスを追加します。 使いやすいコードにしたいので、2つの関数しか追加しません。 join_feed/2leave_feed/2 の2つです。 フィードに参加することを、イベントマネージャの正しいPidとすべてのイベントを転送するPidを入力するだけで可能にすべきです。 join_feed/2 は、 leave_feed/2 でフィードの購読を辞めるときに使われる一意な値を返すべきです。

%% 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 として使った場合でも、まだちゃんと動くでしょうが、そのインスタンスの一つが終了してしまったとき、どのハンドラを削除するかわからなくなってしまいまって、その場合は上手く動作しません。 イベントマネージャは、漠然と1つを取り上げます。 Ref を使うことによって、ヘッド-スマッシュト-イン・バッファロー・ジャンプ出版の社員が現場を抜けることが、エコノミスト誌のジャーナリストを切断することにならないようにしています。(エコノミスト誌がなぜカーリングのレポートをしてるかはしりませんが、知ったことじゃありません) とにかく、次に 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

私たちが自分をフィードに追加して、更新を取得して、それからフィード購読をやめて、受け取るのをやめるところまで見ることが出来ます。 たくさんのプロセスを好きなだけ追加して、ちゃんと動くか実際に試してみてもいいでしょう。

けれどもこれは問題もあります。 カーリングフィードのサブスクライバの1つがクラッシュしたらどうなるでしょう。 ハンドラをそのまま放置しておくのでしょうか。 理想的にはそうしておく必要は無いでしょう。 事実、そうしておく必要はありません。 必要なことは gen_event:add_handler/3 から gen_event:add_sup_handler/3 に変更することだけです。 もしあなたがクラッシュしたら、ハンドラはそこから離れます。 逆に、もし gen_event マネージャがクラッシュしたら、 {gen_event_EXIT, Handler, Reason} というメッセージがあなたに送られるので、対処できます。 簡単ですね。 もう一度考えてみましょう。

19.4.1. Don’t Drink Too Much Kool-Aid

../_images/leash.png

子供の頃に、おばさんやおばあさんのところへパーティーかなにかで言ったことがあったでしょう。 とにかくあなたがいたずらっ子だったら、両親に加えて、あなたのことを見ている大人がさらに何人かいたことでしょう。 あなたがなにかいけないことをしたら、お母さん、お父さん、おばさん、おばあさんから叱られて、そしてもうちゃんとなにがいけなかったかを分かったとあとでさえも、みんなからいろいろ言われつづけることでしょう。 gen_event:add_sup_handler/3 はこれに似ています。真剣にそう思っています。

gen_event:add_sup_handler/3 を使うときはいつでも、プロセスとイベントマネージャの間にリンクがhられるので、両者ともに監視され、ハンドラは親プロセスが落ちたかどうか分かります。 もし エラーとプロセス の章のモニターの節を覚えているでしょうか。そこで私は、モニターは他のプロセスに何が起きているかを知る必要があるライブラリを書くときにとても役に立つと言いました。なぜならリンクと違って、モニターはスタックできるからです。 gen_event はモニターが使える前からErlangに存在し、後方互換性に対する強いコミットメントがこの目の上のたんこぶを残しています。 基本的に、同じプロセスをたくさんのイベントハンドラの親として動作させることができるので、ライブラリは万が一にもプロセスをリンクから外すことは決してしません。(未来永劫終了させる場合は除きます) モニターは実際に問題を解決しますが、そこでは使われていません。

これはつまり、あなたのプロセスがクラッシュしても万事問題ないということです。監視されたハンドラは( YourModule:terminate({stop, Reason}, State) を呼び出して)終了されます。 (イベントマネージャではなく)あなたのハンドラ自身がクラッシュした場合も問題ありません。 {gen_event_EXIT, HandlerId, Reason} を受信するでしょう。 しかし、イベントマネージャが終了するときには、次のどちらかになります:

  • 終了を捕捉していないため {gen_event_EXIT, HandlerId, Reason} メッセージを受信してからクラッシュする
  • {gen_event_EXIT, HandlerId, Reason} メッセージを受信してから、余計あるいは混乱の元である、通常の 'EXIT' メッセージを受信する。

これは、とても気持ち悪くはありますが、少なくとも知っておいたほうがいい動作です。 試して見たければ、あなたのイベントハンドラを監視されたものに切り替えてみるのもいいでしょう。 たとえそれが、ある状況では鬱陶しい作業になるというリスクがあったとしても、そのほうが安全です。 安全第一です。

まだ終わりではありません! 報道機関の社員が定刻に居なかった場合にはどうしたら良いでしょうか。 フィードか試合の現在の状況を彼らに伝えられる必要があります。 これを実現するために、 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 ハンドラを自動で起動するようにしたことにも留意してください。 モジュール自体についても見てみましょう。 カーリングの試合の状況を保持できるべきですね。ここまで、状況を確認するものとしてはチーム、スコア、ラウンドがありました。 これらはすべてstateレコードに保持でき、イベントを受け取るたびに変更できます。 それから、下の例のように、 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.

基本的には誰かが試合の詳細を尋ねてくるまで状況を更新するだけで、ある時点で状況を伝えるだけ、ということがお分かりでしょう。 この処理を非常に基本的な方法で実装しました。 おそらく、コードを整理するより賢い方法は、単純にいままで試合中に起きたすべてのイベントをリストに保存して、新しいプロセスがフィードを購読するたびにそれを一度に送る、というものでした。 ここでは私たちのモジュールがどのように動作するかを見せる上でそのようなものは必要ないので、見ていきましょう:

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 をそれに統合するために必要な概念はすべて見てきました。 もっと大事なことは、ついに活発に活動されているコードで使われている、3つの主なOTPビヘイビアに触れることができたということです。 まだあといくつか触れるべき―すべてのワーカプロセス間の接着剤として動作する―ビヘイビアが残っています。スーパーバイザです。