LoginSignup
3
2

More than 3 years have passed since last update.

すごいE本 第16章 on Elixir (イベントハンドラ)

Last updated at Posted at 2019-04-29

環境

sh
$ 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 の言語設計者は性急に非推奨を発表し混乱を招く人物らしい。

GenEvent — Elixir vx.x.x

上記ドキュメントには代替え案の候補が列挙されている。

  1. Supervisor & GenServer
  2. GenStage
  3. :gen_event

E本のこの段階では 12 は除外だ。
Agent Task を利用したとしてもそれは 1 に内包される。
GenStage は「生産者・消費者」モデルの抽象化なのかな?
それがどうイベントハンドラの代替えになるのかイメージが沸かない。

:gen_event をお勉強していきましょう。

16.2 汎用イベントハンドラ

コールバックの説明をざっと眺めて、後は公式へ。

Erlang -- gen_event

16.3 カーリングの時間です!

カーリングのルールを知らなくても問題なし。

スコアボード

後でスコアボードを gen_event のイベントハンドラとして実装するんだけど、
そのスコアボードのダンプ情報を標準出力するモックを先に作っている。

sh
$ mix new curling
$ cd curling
$ mkdir lib/curling
$ touch lib/curling/dump.ex
curling/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 に渡す。

curling/lib/curling/scoreboard.ex
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
sh
$ mix format
$ iex -S mix
iex
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 に好きなもん渡せるぜってことだね。

とりあえず、インターフェースを整えましょうと続く。

curling/lib/curling.ex
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
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

プレスにアラート!

試合結果の速報フィードも作っちゃうぜー。

curling/lib/curling.ex
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
curling/lib/curling/feed.ex
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
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/3add_sup_handler/3 に変更するだけだよ、と。
この sup はスーパーバイザのことで、ビヘイビアは次の章でお勉強。

実は gen_event はモニターが登場するよりも前から存在するらしく、
gen_sup_hadler/3 は後方互換のために存続しているのだとか。
だから、gen_event の監視にモニターではなくリンクが使われているんだってー。

最後に、上記フィードを累積保存するハンドラを作る。

curling/lib/curling.ex
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
curling/lib/curling/accumulator.ex
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
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 について

GenStage · Elixir School

ああ、なるほど。
これはマルチプロセス版 Stream と認識しておけばいいのかな?

今回のフィードを順序付き辞書ではなく Stream で持っておいて、
購読者からの要求をトリガーとして Enum する。
これをマルチプロセスでやりたいなーという感じ?

だとしたら、やっぱその前にスーパーバイザだね。
おあとがよろしいようで。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2