LoginSignup
9
2

More than 5 years have passed since last update.

はじめてな Elixir(19) GenServer で定期的なお仕事をする(まだ終わってない編)

Last updated at Posted at 2018-10-15

GenServer で定期的に何かする動作を実装してみます。
注:Interface 使った部分についてはまだ終わってません。次回以降も呼んでくださいね。

週末 Elixir

週末恒例のセルフElixir特訓。週末に頭を元に戻すのが大変です。

特にこの前の先週末は別件業務で焼けるほど頭使ったので、脳内コンテキストがそっちに焼き付いちゃって元に戻りません。その前の週末に何やってたんだか皆目見当がつかない。

今となっては別人の自分の書いた記事を一所懸命読むハメになります。敢えて右往左往しているのを残すようにしてまして、そこで「ああでもない、こうでもない」とダラダラ書いてるのが(おいらには)役に立つんです。

そして今週は海外出張です。何が厳しいって、11インチのMac Book Air だと同時に見れる画面数が少な過ぎでプログラミングには大変向いてないんですな。特訓するにしても、Elixir本とかリファレンスとか、あまり見なくてできるような内容に限定されてしまいます。

ちなみに、今は moxy vienna airport というホテルのオープンスペースでハックしてます。ここ、空港徒歩圏内というだけで選んだのですが、ポップで愉快なスペースですっかり気に入ってしまいました。週末の仕事疲れと、長時間フライトの疲れと、時差ボケを癒やしながら、生でダラダラとElixir訓練を再開します。

Interface をきちんと使う

さて GenServer を使うに当たり Interface の考え方を見てみました。プログラミングElixirの 16.5節にもありますし、リファレンスの Client/Server APIs にも出てきます。

要は多様な使い方のできる GenServer (Generic Server) を生で使わずに、そのときどきのプロセスの使い方に応じて限定されたAPIを持つモジュールを作りましょうね、ってなことのようです。

このときのプログラミングスタイルとして @behavior@impl を使うとよろしいようで、上のプログラミングElixirとリファレンスでは十分でないようなので、以下なども参考にしました。
Elixir: Behaviorとはどのような機能なのか? by @ak-ymst さん
Elixir 1.5 で追加された@implを活用しよう by @melpon さん

@behaviour@impl を使って書き換える

はじめてなElixr(16)temp9.ex を書き換えてみましょう。
なお↑の回の @zacky1972 さんのコメントにしたがって、
OKを使ってElixirの :ok, :error タプルをエレガントに処理 by @Tsuyoshi84 さんのネタが使えるように、返り値に :ok :error を返すようにしてます。

temp14.ex
defmodule Temp14 do
  @behaviour GenServer

  @impl GenServer
  def init(_void) do
    {:ok, []}
  end

  @impl GenServer
  def handle_cast(:next, []) do
    {:noreply, [0]}
  end

  @impl GenServer
  def handle_cast(:next, [head | tail]) do
    {:noreply, [head + 1 | [head | tail]]}
  end

  @impl GenServer
  def handle_call(:last, _from, []) do
    {:reply, {:error, []}, []}
  end

  @impl GenServer
  def handle_call(:last, _from, [head | tail]) do
    {:reply, {:ok, head}, [head | tail]}
  end

  def test() do
    {:ok, pid} = GenServer.start_link(__MODULE__, nil)
    IO.inspect(GenServer.call(pid, :last)) # should get :error
    IO.inspect(GenServer.cast(pid, :next))
    IO.inspect(GenServer.call(pid, :last))
    IO.inspect(GenServer.cast(pid, :next))
    IO.inspect(GenServer.call(pid, :last))
    IO.inspect(GenServer.cast(pid, :next))
    IO.inspect(GenServer.call(pid, :last))
    IO.inspect(GenServer.cast(pid, :next))
    IO.inspect(GenServer.call(pid, :last))
    :ok
  end
end

これを実行すると、特に動作には変化がないですけども、
以前より信頼性高くプログラミング出来たということになるかと。

iex.ex
iex(1)> Temp14.test()
{:error, []}
:ok
{:ok, 0}
:ok
{:ok, 1}
:ok
{:ok, 2}
:ok
{:ok, 3}
:ok

後日注:
スミマセン。まだこれだと GenServer が見えているから Interface つかって隠蔽したことになってません。次の回で完成します。もう少しお待ちくださいませ。

GenServer で定期的に自律的な動作をする

はじめてなElixir(18) で、task と agent をつかってプロセスがクライアントからの刺激なしに自分で勝手に何かを実行するメカニズムを作ってみました。これもちろん GenServer 使っても出来ます。リファレンスを読むと handle_info というのを使うとできそうです。

temp15.ex
defmodule Temp15 do
  @behaviour GenServer

  @impl GenServer
  def init(_void) do
    set_interval()
    {:ok, []}
  end

  defp set_interval() do
    Process.send_after(self(), :wakeup, 2 * 1000) # in 2 seconds
  end

  @impl GenServer
  def handle_info(:wakeup, temp_list) do
    new_temp_list = append_new(temp_list)
    set_interval()
    {:noreply, new_temp_list}
  end

  defp append_new([]) do
    [0]
  end

  defp append_new([head | tail]) do
    [head + 1 | [head | tail]]
  end

  @impl GenServer
  def handle_cast(:next, []) do
    {:noreply, [0]}
  end

  @impl GenServer
  def handle_cast(:next, [head | tail]) do
    {:noreply, [head + 1 | [head | tail]]}
  end

  @impl GenServer
  def handle_call(:last, _from, []) do
    {:reply, {:error, []}, []}
  end

  @impl GenServer
  def handle_call(:last, _from, [head | tail]) do
    {:reply, {:ok, head}, [head | tail]}
  end

  @impl GenServer
  def handle_call(:list, _from, list) do
    {:reply, {:ok, list}, list}
  end

set_interval 関数で、定期的(ここでは2秒ごと)に :wakeup を発火?するようにしてます。こうすると handle_info がこれを受け取って、よろしく仕事をします。仕事を終えたら set_interval を忘れずに読んで、次回の仕込みを仕掛けておきます。これで2秒ごとにリストが伸びていくようになります。

なおここでは、わかりやすさのため、敢えてお仕事関数を append_new とか別に定義してありますが、handle_info の定義を繰返してパターンマッチさせるのでも良いです。

iex.ex
iex(1)> {:ok, pid} = GenServer.start_link(Temp15, nil)
{:ok, #PID<0.106.0>}
iex(2)> GenServer.call(pid, :last) # start_link 直後に実行
{:error, []}
iex(3)> GenServer.call(pid, :last) # ちょっとまって実行
{:ok, 0}
iex(4)> GenServer.call(pid, :last)
{:ok, 1}
iex(5)> GenServer.call(pid, :last) # このへんで最初から10秒近く経ってる
{:ok, 4}
iex(6)> GenServer.call(pid, :list) # リストが徐々に伸びていってます
{:ok, [7, 6, 5, 4, 3, 2, 1, 0]}

無事に GenServer でもできました。

参考文献

Elixir リファレンス GenServer の Receiving Regular Messages

9
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
9
2