イベントハンドラ
これを処理しろ! *ショットガンをポンプアクションする*
これまでのいくつかの例では、あえて触れてこなかったことがあります。 リマインダーアプリ を振り返ってみると、IM、メールなど、クライアントに通知する方法について触れたことがわかります。 前の章 では、取引システムは io:format/2 を使用して、何が起こっているかを人々に通知していました。
両方のケースに共通する点がお分かりいただけると思います。どちらも、ある時点で発生したイベントについて、人(またはプロセスやアプリケーション)に知らせることです。一方のケースでは結果を出力するだけでしたが、もう一方のケースでは、メッセージを送信する前にサブスクライバーの Pid を取得しました。
出力アプローチは最小限のものであり、簡単に拡張することはできません。サブスクライバーを使用する方法は確かに有効です。実際、各サブスクライバーがイベントを受信した後に長時間実行する操作がある場合に非常に便利です。より単純なケース、つまり各コールバックに対してイベントを待機するスタンバイプロセスを必ずしも必要としない場合は、第3のアプローチを取ることができます。
この第3のアプローチは、関数を受け入れ、それらを任意の受信イベントで実行させるプロセスを使用するだけです。このプロセスは通常、*イベントマネージャ*と呼ばれ、最終的には次のようになります。
この方法で作業を行うことには、いくつかの利点があります。
- サーバーに多数のサブスクライバーがいる場合、イベントを一度転送するだけで済むため、サーバーは動作を継続できます。
- 転送するデータが多い場合、転送は一度だけ行われ、すべてのコールバックは同じデータインスタンスに対して動作します。
- 短命のタスクのためにプロセスを spawn する必要はありません。
もちろん、いくつかの欠点もあります。
- すべての関数を長時間実行する必要がある場合、それらは互いにブロックします。これは、実際に関数にイベントをプロセスに転送させることで回避できます。基本的に、イベントマネージャをイベントフォワーダーとして機能させることになります(リマインダーアプリで行ったことと同様です)。
- 実際、無限にループする関数は、何かがクラッシュするまで新しいイベントが処理されないようにする可能性があります。
これらの欠点を解決する方法はありますが、少し物足りないかもしれません。基本的には、イベントマネージャアプローチをサブスクライバーアプローチに変える必要があります。幸いなことに、イベントマネージャアプローチは柔軟性があり、簡単にこれを行うことができます。この章の後半でその方法を見ていきます。
通常は、これから説明する OTP ビヘイビアの非常に基本的なバージョンを事前に純粋な Erlang で記述しますが、この場合はすぐに本題に入ります。gen_event の登場です。
汎用イベントハンドラ
gen_event ビヘイビアは、プロセスを実際に開始することがないという点で、gen_server および gen_fsm ビヘイビアとは大きく異なります。「コールバックを受け入れる」ことについて上記で説明した部分が、この理由です。gen_event ビヘイビアは基本的に、関数を受け入れて呼び出すプロセスを実行し、これらの関数を持つモジュールを提供するだけです。つまり、イベント操作に関しては、*イベントマネージャ*が喜ぶ形式でコールバック関数を指定する以外は何もしません。すべての管理は無料で行われます。アプリケーションに固有のものだけを提供します。OTP は、繰り返しますが、汎用的なものと固有のものを分離することであることを考えると、これはそれほど驚くべきことではありません。
しかし、この分離は、標準的な spawn -> init -> loop -> terminate パターンがイベントハンドラにのみ適用されることを意味します。前に述べたことを思い出してください。イベントハンドラは、マネージャで実行される関数の束です。つまり、現在提示されているモデル
は、プログラマーにとっては次のようなものになります。
各イベントハンドラは、マネージャによって持ち運ばれる独自のステートを保持できます。各イベントハンドラは、次の形式を取ることができます。
これはそれほど複雑なものではないので、イベントハンドラのコールバック自体について説明しましょう。
init と terminate
init 関数と terminate 関数は、サーバーと有限状態マシンを持つ以前のビヘイビアで見たものと似ています。init/1 は引数のリストを受け取り、{ok, State} を返します。init/1 で発生したことはすべて、terminate/2 に対応するものが必要です。
handle_event
handle_event(Event, State) 関数は、多かれ少なかれ gen_event のコールバックモジュールの中核です。これは、非同期で動作するという点で、gen_server の handle_cast/2 と同様に動作します。ただし、返せる値については異なります。
{ok, NewState}{ok, NewState, hibernate}。これは、次のイベントが発生するまでイベントマネージャ自体を休止状態にします。remove_handler{swap_handler, Args1, NewState, NewHandler, Args2}
タプル {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_server:cast/2 と同様に非同期である gen_event:notify/2 から発生する可能性があります。同期である gen_event:sync_notify/2 もあります。handle_event/2 は非同期のままなので、これは少しおかしな言い方です。ここでの考え方は、すべてのイベントハンドラが新しいメッセージを見て処理した後にのみ関数呼び出しが返されるということです。それまでは、イベントマネージャは応答しないことで呼び出しプロセスをブロックし続けます。
handle_call
これは gen_server の handle_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 個の返信を期待するのでしょうか、それともすべてを含む 1 つの返信を期待するのでしょうか?実際には、返信するハンドラを 1 つだけ選択する必要があります。これがどのように行われるかについては、実際にハンドラをイベントマネージャにアタッチする方法を説明する際に詳しく説明しますが、せっかちな場合は、gen_event:add_handler/3 関数がどのように機能するかを見て、理解してみてください。
handle_info
handle_info/2 コールバックは、帯域外メッセージ(終了シグナル、! 演算子を使用してイベントマネージャに直接送信されたメッセージなど)のみを処理するという点を除いて、handle_event とほぼ同じです(戻り値なども同じです)。gen_server および gen_fsm の handle_info と同様のユースケースがあります。
code_change
コード変更は、個々のイベントハンドラに対して行われることを除いて、gen_server とまったく同じ方法で機能します。3 つの引数、OldVsn、State、および Extra を取ります。これらは順番に、バージョン番号、現在のハンドラの状態、および今のところ無視できるデータです。必要なのは、{ok, NewState} を返すことだけです。
カーリングの時間だ!
コールバックを確認したので、gen_event を使用した実装を見ていきましょう。この章では、世界で最も面白いスポーツの 1 つであるカーリングのゲームの更新を追跡するために使用される一連のイベントハンドラを作成することにしました。
カーリングを見たことがない、またはプレイしたことがない場合(残念です!)、ルールは比較的簡単です。
2 つのチームがあり、彼らは カーリングストーン を氷の上で滑らせて、赤い円の真ん中に到達させようとします。彼らは 16 個のストーンでこれを行い、ラウンドの終わり(*エンド*と呼ばれる)に中央に最も近いストーンを持つチームがポイントを獲得します。チームが最も近い 2 つのストーンを持っている場合、2 ポイントを獲得します。10 エンドあり、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").
これでスコアボードの全機能が出揃いました。通常、タイマーやその他の素晴らしい機能がありますが、まあいいでしょう。オリンピック委員会は、チュートリアルで些細なことを実装させたくないようです。
このハードウェアインターフェースにより、少しばかり設計時間を確保できます。今のところ、処理する必要があるイベントはいくつかあります。チームの追加、次のラウンドへの移行、ポイント数の設定です。 `reset_board` 機能は新しいゲームを開始するときにのみ使用し、プロトコルの一部としては必要ありません。必要なイベントは、プロトコルで次の形式をとる場合があります。
- `{set_teams, TeamA, TeamB}`。これは `curling_scoreboard_hw:set_teams(TeamA, TeamB)` の単一の呼び出しに変換されます。
- `{add_points, Team, N}`。これは `curling_scoreboard_hw:add_point(Team)` の N 回の呼び出しに変換されます。
- `next_round`。これは同じ名前の単一の呼び出しに変換されます。
この基本的なイベントハンドラスケルトンを使用して実装を開始できます。
-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()` を一意の値として使用することです。この値をハンドラに渡すには、`gen_event:add_handler(Pid, {Module, Ref}, Args)` として `add_handler/3` を呼び出して追加します。この時点から、`{Module, Ref}` を使用して、その特定のハンドラと通信できます。問題は解決しました。
とにかく、イベントハンドラにメッセージを送信し、ハードウェアモジュールを正常に呼び出していることがわかります。その後、ハンドラを削除します。ここで、`turn_off` は `terminate/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
これは大きな利点には見えませんが、実際にはコードを使いやすくすること(そしてメッセージを間違って書く可能性を減らすこと)が目的です。
記者に知らせろ!
基本的なスコアボードが完成したので、今度は国際的な記者がシステムの更新担当者からライブデータを取得できるようにしたいと考えています。これはサンプルプログラムなので、ソケットを設定して更新用のプロトコルを作成する手順は省略しますが、仲介プロセスを担当させることで、システムを配置します。
基本的に、報道機関がゲームフィードに参加したい場合は、必要なデータを転送するだけの独自のハンドラを登録します。事実上、gen_eventサーバーを一種のメッセージハブに変え、必要な人にルーティングします。
最初に行うことは、curling.erl モジュールを新しいインターフェースで更新することです。使いやすくするために、`join_feed/2` と `leave_feed/2` の2つの関数のみを追加します。フィードへの参加は、イベントマネージャの適切なPidとすべてのイベントを転送するPidを入力するだけで実行できるはずです。これにより、`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` として使用した場合でも、問題は発生しませんでしたが、インスタンスが不要になったときにどのハンドラを削除するかを制御できなくなります。イベントマネージャは、未定義の方法でいずれかを選択します。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.
ここで興味深いのは、すべてのイベントを登録しているPidに盲目的に転送する `handle_event/2` 関数だけです。新しいモジュールを使用する場合
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}` が返送されるため、処理できます。簡単ですよね?もう一度考えてみてください。
クールエイドを飲みすぎないでください
子供の頃にパーティーなどで叔母や祖母の家に遊びに行ったことがあるかもしれません。もしあなたが少しでもいたずら好きなら、両親に加えて、何人かの大人があなたを見守っていたでしょう。もしあなたが何か悪いことをしたら、お母さん、お父さん、叔母、祖母に叱られ、その後もあなたがすでに自分が悪いことをしたことをはっきりと知っていても、みんながあなたに言い続けるでしょう。まあ、`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}` が受信されます。ただし、イベントマネージャがシャットダウンされると、次のいずれかになります。
- `{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` 関数はハンドラIDとして `curling_accumulator` のみを 使用していることです。同じハンドラの複数のバージョンがある場合、`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.
基本的に、誰かがゲームの詳細を尋ねるまで状態を更新し、その時点で詳細を返信します。これは非常に基本的な方法で行いました。おそらく、コードを整理するより賢明な方法は、ゲームで発生したすべてのイベントのリストを保持するだけで、新しいプロセスがフィードに登録されるたびに、一度にすべてを返信できるようにすることでした。ここでは、物事の仕組みを示すためにこれは必要ありません。したがって、新しいコードを使用することに焦点を当てましょう。
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 の最も一般的な用途は説明しませんでしたが、それらを理解し、独自のイベントハンドラを構築し、アプリケーションに統合するために必要な概念はすべて説明しました。さらに重要なのは、アクティブなコード開発で使用される3つの主要なOTPビヘイビアをすべて網羅したことです。まだいくつかのビヘイビアが残っています。スーパーバイザーのように、すべてのワーカープロセス間の接着剤として機能するビヘイビアです。