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 タプルをエレガントに処理] (https://qiita.com/Tsuyoshi84/items/5a020ef185b094778d92) by @Tsuyoshi84 さんのネタが使えるように、返り値に :ok
:error
を返すようにしてます。
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(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
というのを使うとできそうです。
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(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 でもできました。