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

# 環境

```shell-session: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](https://hexdocs.pm/elixir/GenEvent.html#content)

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

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

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

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

# 16.2 汎用イベントハンドラ

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

[Erlang -- gen_event](http://erlang.org/doc/man/gen_event.html)

# 16.3 カーリングの時間です！

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

## スコアボード

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

```shell-session:sh
$ mix new curling
$ cd curling
$ mkdir lib/curling
$ touch lib/curling/dump.ex
```
```elixir: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} のスコアを１増加")
  end

  def reset_board do
    IO.puts("Scoreboard: 全チームを未定義、全スコアを０に設定")
  end
end
```

## ゲームイベント

スコアボードを `gen_event` のイベントハンドラとして実装。
こいつを後で `:gen_event.add_handler/3` に渡す。

```elixir: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
```
```shell-session:sh
$ mix format
$ iex -S mix
```
```elixir: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 のスコアを１増加
Scoreboard: Pirates のスコアを１増加
Scoreboard: Pirates のスコアを１増加
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` の第３引数 `:turn_off` は特に意味を持たない例示。
`terminate/2` に好きなもん渡せるぜってことだね。

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

```elixir: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
```
```elixir: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 のスコアを１増加
Scoreboard: Scotsmen のスコアを１増加
iex(3)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
```

## プレスにアラート！

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

```elixir: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
```
```elixir: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
```
```elixir: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 のスコアを１増加
Scoreboard: Saskatchewan Roughriders のスコアを１増加
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` の監視にモニターではなくリンクが使われているんだってー。

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

```elixir: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
```
```elixir: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
    # ２番目の引数 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
```
```elixir: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 のスコアを１増加
Scoreboard: Pigeons のスコアを１増加
iex(3)> Curling.next_round(pid)
Scoreboard: ラウンド終了
:ok
iex(4)> Curling.add_points(pid, "Eagles", 3) 
Scoreboard: Eagles のスコアを１増加
:ok
Scoreboard: Eagles のスコアを１増加
Scoreboard: Eagles のスコアを１増加
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](https://elixirschool.com/ja/lessons/advanced/gen-stage/)

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

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

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