自分は、Elixirで一定間隔で繰り返し実行される処理を:timer.sleep/1
と再帰処理の組み合わせで実施していた。
しかし、GerServerを利用する分には、Process.send_after/4
とGenServer.handle_info/2
を利用すれば、シンプルに実装できることを先日知った。
具体的な対応方法としては、GenServer.init/1
中でProcess.send_after/4
を実行。指定の時間後にGenServer.handle_info/2
が呼び出されるので、その中で再びProcess.send_after/4
を呼び出せばよい。
元ネタはこちら。
また、Process.send_after/4
の詳細はこちらを参照。
実装
一定間隔でFizzBuzzの結果を返すGenServerのコードを書いてみる。
まず、プロジェクトの作成。
「Supervisor」ありでプロジェクト作成をしているが、別に無くても良い。
$mix new fizzbuzz --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/fizzbuzz.ex
* creating lib/fizzbuzz/application.ex
* creating test
* creating test/test_helper.exs
* creating test/fizzbuzz_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd fizzbuzz
mix test
Run "mix help" for more commands.
つぎに、定期実行されるGernServerのコードを用意する。
内容としては、一定間隔でFizzBuzzの値がコンソールに出力されるようにした。
GenServerを利用しているので、FizzBuzzの結果は"状態"の値を利用して算出する実装とした。そのため、FizzBuzz実行後の"状態"はインクリメントしていく。
defmodule Fizzbuzz.Auto do
use GenServer
def calc_fb({n, 0, 0}), do: {n, "FizzBuzz"}
def calc_fb({n, 0, _}), do: {n, "Fizz"}
def calc_fb({n, _, 0}), do: {n, "Buzz"}
def calc_fb({n, _, _}), do: {n, to_string(n)}
def fizzbuzz(n) do
cond do
n < 1 -> {n , "Cannot calculate"}
true -> calc_fb({n, rem(n,3), rem(n, 5)})
end
end
def schedule_work do
# In 5 Seconds
Process.send_after(self(), :work, 5 * 1000)
end
# 以降、GenServerの実装
def init(state) do
IO.puts("Fizzbuzz.Auto: init/1 call")
schedule_work()
{:ok, state}
end
def handle_info(:work, current_num) do
{n, v} = current_num |> fizzbuzz()
IO.puts("Fizzbuzz.Auto: schedule_work : #{n} : #{v}")
# Reschedule once more
schedule_work()
{:noreply, current_num + 1}
end
end
init/1
から、Process.send_after/4
を実行する関数schedule_work/0
を呼び出す。この時、Process.send_after/4
では、引数に「自分自身のPID」「メッセージ(今回は:work
)」「待ち時間(今回は5000ms
)」を渡してる。
5秒(5000ms)後に、handle_info(:work, current_num)
が呼び出され、この中でFizzBuzz処理の実行後にふたたび関数schedule_work/0
が呼び出される。
実行結果
実際に動かしてみる。
iex -S mix
でiexを起動後、GenServer.start(Fizzbuzz.Auto, 1)
でFizzbuzz.Autoを実行する。ここで、「1
」は初期の"状態"である。
$iex -S mix
Erlang/OTP 22 [erts-10.4.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Compiling 3 files (.ex)
Generated fizzbuzz app
Interactive Elixir (1.9.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> GenServer.start(Fizzbuzz.Auto, 1)
Fizzbuzz.Auto: init/1 call
{:ok, #PID<0.168.0>}
Fizzbuzz.Auto: schedule_work : 1 : 1
Fizzbuzz.Auto: schedule_work : 2 : 2
Fizzbuzz.Auto: schedule_work : 3 : Fizz
Fizzbuzz.Auto: schedule_work : 4 : 4
Fizzbuzz.Auto: schedule_work : 5 : Buzz
Fizzbuzz.Auto: schedule_work : 6 : Fizz
Fizzbuzz.Auto: schedule_work : 7 : 7
Fizzbuzz.Auto: schedule_work : 8 : 8
Fizzbuzz.Auto: schedule_work : 9 : Fizz
Fizzbuzz.Auto: schedule_work : 10 : Buzz
Fizzbuzz.Auto: schedule_work : 11 : 11
Fizzbuzz.Auto: schedule_work : 12 : Fizz
Fizzbuzz.Auto: schedule_work : 13 : 13
Fizzbuzz.Auto: schedule_work : 14 : 14
Fizzbuzz.Auto: schedule_work : 15 : FizzBuzz
Fizzbuzz.Auto: schedule_work : 16 : 16
〜(以下、続いていく)〜
実行すると、5秒ごとにメッセージが出力される。
課題
Process.send_after/4
の読み出しをinit/1
の時点から開始しないと、うまく動いてくれない。
例えば、init/1
でschedule_work/0
を呼び出ないでGenServerとして起動し、その後にschedule_work/0
を実行しても、iex上では何も起こっていないように見える。
$cat ./lib/fizzbuzz/fizzbuzz_auto.ex
〜(略)〜
def init(state) do
IO.puts("Fizzbuzz.Auto: init/1 call")
# schedule_work()
{:ok, state}
end
〜(略)〜
$iex -S mix
iex(1)> GenServer.start(Fizzbuzz.Auto, 1)
Fizzbuzz.Auto: init/1 call
{:ok, #PID<0.146.0>}
iex(2)> Fizzbuzz.Auto.schedule_work()
#Reference<0.2803571744.3830972420.104800>
iex(3)>
この辺りの理屈はまだ理解しきれていないので、わかったら追記していきたい。