環境
$ lsb_release -d
Description: Ubuntu 18.04.2 LTS
$ elixir -v
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.8.1 (compiled with Erlang/OTP 20)
そう遠くない昔、Elixir には GenEvent
が存在した。(あ、まだ使えるか)
Elixir の言語設計者は性急に非推奨を発表し混乱を招く人物らしい。
上記ドキュメントには代替え案の候補が列挙されている。
-
Supervisor
&GenServer
GenStage
:gen_event
E本のこの段階では 1
と 2
は除外だ。
Agent
Task
を利用したとしてもそれは 1
に内包される。
GenStage
は「生産者・消費者」モデルの抽象化なのかな?
それがどうイベントハンドラの代替えになるのかイメージが沸かない。
:gen_event
をお勉強していきましょう。
16.2 汎用イベントハンドラ
コールバックの説明をざっと眺めて、後は公式へ。
16.3 カーリングの時間です!
カーリングのルールを知らなくても問題なし。
スコアボード
後でスコアボードを gen_event
のイベントハンドラとして実装するんだけど、
そのスコアボードのダンプ情報を標準出力するモックを先に作っている。
$ mix new curling
$ cd curling
$ mkdir lib/curling
$ touch lib/curling/dump.ex
defmodule Curling.Dump do
def set_teams(team_a, team_b) do
IO.puts("Scoreboard: #{team_a} vs #{team_b}")
end
def next_round do
IO.puts("Scoreboard: ラウンド終了")
end
def add_point(team) do
IO.puts("Scoreboard: #{team} のスコアを1増加")
end
def reset_board do
IO.puts("Scoreboard: 全チームを未定義、全スコアを0に設定")
end
end
ゲームイベント
スコアボードを gen_event
のイベントハンドラとして実装。
こいつを後で :gen_event.add_handler/3
に渡す。
defmodule Curling.ScoreBoard do
alias Curling.Dump
###
# Callbacks
def init([]), do: {:ok, []}
def handle_event({:set_teams, team_a, team_b}, state) do
Dump.set_teams(team_a, team_b)
{:ok, state}
end
def handle_event({:add_points, team, n}, state) do
1..n
|> Enum.each(fn _ -> Dump.add_point(team) end)
{:ok, state}
end
def handle_event(:next_round, state) do
Dump.next_round()
{:ok, state}
end
def handle_event(_event, state) do
{:ok, state}
end
# term: Erlang/Elixir の項
def handle_call(_term, state) do
{:ok, :ok, state}
end
def handle_info(_term, state) do
{:ok, state}
end
end
$ mix format
$ iex -S mix
iex(1)> {:ok, pid} = :gen_event.start_link()
{:ok, #PID<0.149.0>}
iex(2)> :gen_event.add_handler(pid, Curling.ScoreBoard, [])
:ok
iex(3)> :gen_event.notify(pid, {:set_teams, "Pirates", "Scotsmen"})
Scoreboard: Pirates vs Scotsmen
:ok
iex(4)> :gen_event.notify(pid, {:add_points, "Pirates", 3})
:ok
Scoreboard: Pirates のスコアを1増加
Scoreboard: Pirates のスコアを1増加
Scoreboard: Pirates のスコアを1増加
iex(5)> :gen_event.notify(pid, :next_round)
Scoreboard: ラウンド終了
:ok
iex(6)> :gen_event.delete_handler(pid, Curling.ScoreBoard, :turn_off)
:ok
iex(7)> :gen_event.notify(pid, :next_round)
:ok
複数のハンドラを追加した場合はどう見分けるの?
それには make_ref/0
で参照をとって、
:gen_event.add_handler(pid, {mod, ref}, args})
を使うんだよ、と。
上記の delete_handler
の第3引数 :turn_off
は特に意味を持たない例示。
terminate/2
に好きなもん渡せるぜってことだね。
とりあえず、インターフェースを整えましょうと続く。
defmodule Curling do
alias __MODULE__, as: Me
def start_link(team_a, team_b) do
{:ok, pid} = :gen_event.start_link()
:gen_event.add_handler(pid, Me.ScoreBoard, [])
set_teams(pid, team_a, team_b)
{:ok, pid}
end
def set_teams(pid, team_a, team_b) do
:gen_event.notify(pid, {:set_teams, team_a, team_b})
end
def add_points(pid, team, n) do
:gen_event.notify(pid, {:add_points, team, n})
end
def next_round(pid) do
:gen_event.notify(pid, :next_round)
end
end
iex(1)> {:ok, pid} = Curling.start_link("Pirates", "Scotsmen")
Scoreboard: Pirates vs Scotsmen
{:ok, #PID<0.136.0>}
iex(2)> Curling.add_points(pid, "Scotsmen", 2)
:ok
Scoreboard: Scotsmen のスコアを1増加
Scoreboard: Scotsmen のスコアを1増加
iex(3)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
プレスにアラート!
試合結果の速報フィードも作っちゃうぜー。
defmodule Curling do
alias __MODULE__, as: Me
...
# 末尾に追加
def join_feed(pid, to_pid) do
handler_id = {Me.Feed, make_ref()}
:gen_event.add_handler(pid, handler_id, [to_pid])
handler_id
end
def leave_feed(pid, handler_id) do
:gen_event.delete_handler(pid, handler_id, :leave_feed)
end
end
defmodule Curling.Feed do
###
# Callbacks
def init([pid]), do: {:ok, pid}
def handle_event(event, pid) do
send(pid, {:curling_feed, event})
{:ok, pid}
end
def handle_call(_term, pid), do: {:ok, :ok, pid}
def handle_info(_term, pid), do: {:ok, pid}
def code_change(_old_vsn, pid, _extra), do: {:ok, pid}
def terminate(_reason, _pid), do: :ok
end
iex(1)> {team_a, team_b} = {"Saskatchewan Roughriders", "Ottawa Roughriders"}
{"Saskatchewan Roughriders", "Ottawa Roughriders"}
iex(2)> {:ok, pid} = Curling.start_link(team_a, team_b)
Scoreboard: Saskatchewan Roughriders vs Ottawa Roughriders
{:ok, #PID<0.145.0>}
iex(3)> handler_id = Curling.join_feed(pid, self())
{Curling.Feed, #Reference<0.538009435.4198236162.7166>}
iex(4)> Curling.add_points(pid, team_a, 2)
:ok
Scoreboard: Saskatchewan Roughriders のスコアを1増加
Scoreboard: Saskatchewan Roughriders のスコアを1増加
iex(5)> flush()
{:curling_feed, {:add_points, "Saskatchewan Roughriders", 2}}
:ok
iex(6)> Curling.leave_feed(pid, handler_id)
:ok
iex(7)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
iex(8)> flush()
:ok
このとき、フィードの購読者がクラッシュしたらハンドラも消えてほしいよね。
それには add_handler/3
を add_sup_handler/3
に変更するだけだよ、と。
この sup
はスーパーバイザのことで、ビヘイビアは次の章でお勉強。
実は gen_event
はモニターが登場するよりも前から存在するらしく、
gen_sup_hadler/3
は後方互換のために存続しているのだとか。
だから、gen_event
の監視にモニターではなくリンクが使われているんだってー。
最後に、上記フィードを累積保存するハンドラを作る。
defmodule Curling do
alias __MODULE__, as: Me
def start_link(team_a, team_b) do
{:ok, pid} = :gen_event.start_link()
:gen_event.add_handler(pid, Me.ScoreBoard, [])
:gen_event.add_handler(pid, Me.Accumulator, []) # 追加
set_teams(pid, team_a, team_b)
{:ok, pid}
end
...
# 追加
def game_info(pid) do
:gen_event.call(pid, Me.Accumulator, :game_data)
end
end
defmodule Curling.Accumulator do
alias __MODULE__, as: Me
defstruct teams: :orddict.new(), round: 0
def init([]), do: {:ok, %Me{}}
def handle_event({:set_teams, team_a, team_b}, me = %Me{}) do
# Erlang は操作対象が最後の引数にくるので注意(key, value, dst)
teams = :orddict.store(team_a, 0, me.teams)
teams = :orddict.store(team_b, 0, teams)
{:ok, %Me{me | teams: teams}}
end
def handle_event({:add_points, team, n}, me = %Me{}) do
# 2番目の引数 n で key の value をインクリメントする
teams = :orddict.update_counter(team, n, me.teams)
{:ok, %Me{me | teams: teams}}
end
def handle_event(:next_round, me = %Me{}) do
{:ok, %Me{me | round: me.round + 1}}
end
def handle_event(_event, me = %Me{}), do: {:ok, me}
def handle_call(:game_data, me = %Me{}) do
reply = {:orddict.to_list(me.teams), {:round, me.round}}
{:ok, reply, me}
end
def handle_call(_term, me = %Me{}), do: {:ok, :ok, me}
def handle_info(_term, me = %Me{}), do: {:ok, me}
def code_change(_old_vsn, me = %Me{}, _extra), do: {:ok, me}
def terminate(_reason, %Me{}), do: :ok
end
iex(1)> {:ok, pid} = Curling.start_link("Pigeons", "Eagles")
Scoreboard: Pigeons vs Eagles
{:ok, #PID<0.144.0>}
iex(2)> Curling.add_points(pid, "Pigeons", 2)
:ok
Scoreboard: Pigeons のスコアを1増加
Scoreboard: Pigeons のスコアを1増加
iex(3)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
iex(4)> Curling.add_points(pid, "Eagles", 3)
Scoreboard: Eagles のスコアを1増加
:ok
Scoreboard: Eagles のスコアを1増加
Scoreboard: Eagles のスコアを1増加
iex(5)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
iex(6)> Curling.game_info(pid)
{[{"Eagles", 3}, {"Pigeons", 2}], {:round, 2}}
イベントハンドラが最も利用されるケースはロギングとシステムアラートなんだよ。
実例はソースコードなど巷を徘徊してねと。
次章はビヘイビアの大トリ、スーパーバイザだ。
その前に GenStage
も見ておくかなー。
※ GenStage
について
ああ、なるほど。
これはマルチプロセス版 Stream
と認識しておけばいいのかな?
今回のフィードを順序付き辞書ではなく Stream
で持っておいて、
購読者からの要求をトリガーとして Enum
する。
これをマルチプロセスでやりたいなーという感じ?
だとしたら、やっぱその前にスーパーバイザだね。
おあとがよろしいようで。