【LiveView編】GenServerのhandle_info/2とProcess.send_after/4の組み合わせによる定期実行

`Process.send_after/4`と`GenServer.handle_info/2`を利用すれば定期的な処理が実行できることを先日知りました。

[前回](https://qiita.com/MzRyuKa/items/268bc4824703feaf8b2c)は素のGenServerで実施してみたので、今回は**Phoenix LiveView**で実施してみました。

## 最初に結論

Phoenix LiveViewでも問題なく実行できました。
画面中では以下の2つの定期更新が行われています。
　* **1秒間隔で更新される時刻**
　* **3秒ごとに更新されるFizzBuzzの値**

![liveview_auto_fizzbuzz_20200224_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316636/61bc9fe7-28f2-20b4-a85f-cda106a7fb76.gif)

## 実装

以下の環境で動作確認しました。

 * Elixir 1.9.4-otp-22
 * Phoenix Framework : 1.4.14
 * Phoexix LiveView : 0.7.1

Phoenix LiveViewはバージョンによる差異が割と出てくるので、Phoenix LiveView側の設定周りなどの説明は省略。
ただし、試した内容自体はLiveViewのバージョンにはほとんど依存しないので、0.7.1よりも古いバージョンでも動くはず。

### コード

実装コードの`fizzbuzz.ex`と画面テンプレートの`autofizzbuzz.html.leex`を載せます。
自分は好みでrenderの中身は外部ファイルにしていますが、今回のレベルでなら`~L`を利用して`fizzbuzz.ex`内に記述した方が簡潔ではありますね。


```elixir:lib/hello_web/live/fizzbuzz.ex
defmodule HelloWeb.Fizzbuzz do
  use Phoenix.LiveView

  import Calendar.Strftime

  def render( assigns ) do
    HelloWeb.LiveView.render("autofizzbuzz.html",assigns)
  end

  ## 補足： LiveViewが0.5.2より以前の場合は、mount/2になります。
  ## 　 　 _paramsをなくして、mount(_session, socket )になります。
  def mount(_params, _session, socket ) do
    if connected?(socket) do
      Process.send_after(self(), :tick, 1000)
      Process.send_after(self(), :fizzbuzz, 3000)
    end
    { :ok, assign( socket,
                   time: local_time(),
                   num: 0,
                   val: "initial" ) }
  end

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, 1000)
    {:noreply, assign(socket, time: local_time())}
  end

  def handle_info(:fizzbuzz, socket) do
    Process.send_after(self(), :fizzbuzz, 3000)
    next_num = socket.assigns.num + 1
    {num, val} = fizzbuzz(next_num)
    {:noreply, assign( socket, num: num, val: val ) }
  end

  ##### local

  defp local_time() do
    :calendar.local_time() |> strftime!("%r")
  end

  defp calc_fb({n, 0, 0}), do: {n, "FizzBuzz"}
  defp calc_fb({n, 0, _}), do: {n, "Fizz"}
  defp calc_fb({n, _, 0}), do: {n, "Buzz"}
  defp calc_fb({n, _, _}), do: {n, to_string(n)}

  defp fizzbuzz(n) do
    cond do
      n < 1 -> {n , "Cannot calculate"}
      true  -> calc_fb({n, rem(n,3), rem(n, 5)})
    end
  end
end
```

```elixir:lib/hello_web/templates/live/autofizzbuzz.html.leex
<h2>ここに現在時刻が表示されます。</h2>

<span><%= @time %></span>

<h2>ここにFizzBuzzの値が表示されます。</h2>

<span><%= @num %>:<%= @val %></span>
```

### 解説

ポイントだけ。

 * 初期画面表示時に呼ばれる`mount/3`中で、2つの`Process.send_after`をcallしています。
   * メッセージ「`:tick`」を1秒（1000ms）間隔で。
   * メッセージ「`:fizzbuzz`」を3秒（3000ms）間隔で。
   * なお、どちらもコネクションが確立されてる場合のみ実行。
 * メッセージ「`:tick`」のなかで、再びメッセージ「`:tick`」の`handle_info/2`をcall。
 * メッセージ「`:fizzbuzz`」のなかで、再びメッセージ「`:fizzbuzz`」の`handle_info/2`をcall。
 * FizzBuzzに使う数値は`socket`中から取得する。
   * `socket.assigns.＜項目名＞`で取得します。
   * コード中にも記載していますが、`@num`を取得する場合は、`socket.assigns.num`と指定します。

それと、コードのコメントにも書いていますが、LiveViewのコード中で必須の`mount関数`が、v0.5.2までは`mount/2`だったのに対して、v0.6.0以降は`mount/3`となっています。
`mount/3`では第一引数に`params`を用意してやる必要がありますが、今回の例で`params`は利用していないのでバージョンの違いによる差異は感じません。

[v0.5.2](https://hexdocs.pm/phoenix_live_view/0.5.2/Phoenix.LiveView.html#module-example)
[v0.6.0](https://hexdocs.pm/phoenix_live_view/0.6.0/Phoenix.LiveView.html#module-example)

## まとめ

GenServerがベースになっているので当たり前といえば当たり前なんですが、Phoenix LiveViewと「`Process.send_after/4`+`handle_info/2`の組み合わせ」の相性は良さげです。

## 参考情報

 * https://hexdocs.pm/elixir/GenServer.html#module-receiving-regular-messages
 * https://hexdocs.pm/elixir/Process.html#send_after/4
 * https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript
 * https://hexdocs.pm/phoenix_live_view/0.7.1/Phoenix.LiveView.html
 * https://qiita.com/MzRyuKa/items/268bc4824703feaf8b2c