はじめに
今日は fukuoka.ex のもくもく会です。fukuoka.ex のもくもく会どころか「もくもく」と名前がついている会合すら初めてです。ここでやったことをそのまま貼り付けてみます。
これまで
はじめてなElixir(9) 有限状態機械を作ってみる
はじめてなElixir(10) 小水力発電所の状態遷移を表現してみる
と、状態機械で遊んできましたが、もうちょっと先に行ってみようと思います。そのために今日は生プロセスにチャレンジしてみます。
とりあえず教科書のまま
まずは「プログラミングElixir」14章の先頭のママで遊んでみます。
defmodule Spawn4 do
def greet do
receive do
{sender, msg} ->
send sender, { :ok, "Hello, #{msg}" }
greet()
end
end
end
pid = spawn(Spawn4, :greet, [])
send pid, {self(), "World!"}
receive do
{:ok, message} ->
IO.puts message
end
send pid, {self(), "Kermit!"}
receive do
{:ok, message} ->
IO.puts message
after 500 ->
IO.puts "The greeter has gone away"
end
実行してみると、教科書通り、動く!
$ iex spawn4.ex
Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Hello, World!
Hello, Kermit!
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
プロセスの振る舞いをパターンマッチで変えてみる
次にしたいことの下心があって、プロセスの振る舞いを変えてみます。構文見ると receive で受け取るメッセージでパターンマッチできそうなので、そこで条件分岐させてみます。
defmodule Spawn5 do
def greet do
receive do
{sender, {:bar, msg}} ->
send(sender, { :ok, "Hello0, #{msg}" })
greet()
{sender, {:foo, msg}} ->
send(sender, { :ok, "Hello1, #{msg}" })
greet()
{sender, msg} ->
send(sender, { :ok, "Hello2, #{msg}" })
greet()
end
end
end
pid = spawn(Spawn5, :greet, [])
send(pid, {self(), {:bar, "World!"}})
receive do
{:ok, message} ->
IO.puts message
end
send(pid, {self(), {:foo, "Kermit!"}})
receive do
{:ok, message} ->
IO.puts message
after 500 ->
IO.puts "The greeter has gone away"
end
send(pid, {self(), "Kochi!"})
receive do
{:ok, message} ->
IO.puts message
after 500 ->
IO.puts "The greeter has gone away"
end
:bar, :foo, キーワードなし、で分岐です。
これを実行すると、うまいこと動いてくれました。
(とか調子よく書いてますが、ここに至るのにエラーしてエラーして1時間かかってます。)
iex(1)> c "spawn5.ex"
Hello0, World!
Hello1, Kermit!
Hello2, Kochi!
[Spawn5]
iex(2)>
状態を持たせてみちゃおうかなぁ
いいのかなぁ、いいのかなぁ、それももくもく会でやっちゃってもいいのかなぁ。良い子は気をつけましょう。
明示的な状態ではもたせられない
なにかグローバルな変数を持っててそれを持ち回るってのはできないのか、私が知らないのか、とにかくできそうにないです。そういう言語ですから。んで、どうするかというと、今回は関数の引数で持ってみるというのをやってみます。
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, {:ok, "Initial temperature is #{init_temp}"})
pseudo_temp_sensor(init_temp)
{sender, :next} ->
cur = temp
send(sender, {:ok, "Next temperature is #{cur}"})
pseudo_temp_sensor(cur)
end
end
なんちゃって温度センサという名前で関数をつくってみました。
- :init 付きで呼び出すと、その値を使って呼び出したプロセスに返事をしてから、自分自身を呼び出す再帰呼び出し(狙ってるのは無限ループ)に入ります。
- :next 付きで呼び出すと、前の値を呼び出し元のプロセスに返事をしてから、やはり自分自身を呼び出します。
これをテストしてみましょう。テストも iex で手で毎回打ってると大変なので、プログラムを組んでしまいます。
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do
{:ok, message} -> IO.puts message
after 500 ->
IO.puts "The process has gone away"
end
end
def test_sensor do
pid = spawn(Spawn6, :pseudo_temp_sensor, [:nil])
send(pid, {self(), {:init, 0}})
receive do
{:ok, message} -> IO.puts message
end
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
end
end
これを動かすとこんな風に動きます。
iex(3)> c "spawn6.ex"
warning: redefining module Spawn6 (current version defined in memory)
spawn6.ex:1
[Spawn6]
iex(4)> Spawn6.test_sensor
Initial temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
Next temperature is 0
:ok
iex(5)>
値が変わらないですが、next で前の値を持ち回ってるはずです。
ちょっと値を変えてみる
これだとつまらないので :next 付きで呼び出すと、前の値を元にちょっとだけずれた値を、呼び出し元のプロセスに返事をするようにします。その後、やはり自分自身を呼び出します。
先程の
cur = temp
を
cur = temp + Enum.random(0..9) * Enum.random(-1..1)
としてみます。
defmodule Spawn7 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, {:ok, "Initial temperature is #{init_temp}"})
pseudo_temp_sensor(init_temp)
{sender, :next} ->
cur = temp + Enum.random(0..9) * Enum.random(-1..1)
send(sender, {:ok, "Next temperature is #{cur}"})
pseudo_temp_sensor(cur)
end
end
ちょっとずつ値が変わるはずです。テストプログラムをこんな感じで。
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do
{:ok, message} -> IO.puts message
after 500 ->
IO.puts "The process has gone away"
end
end
def test_sensor_repeat(pid) do
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
test_next_temp(pid)
end
def test_sensor(zero_th) do
pid = spawn(Spawn7, :pseudo_temp_sensor, [:nil])
send(pid, {self(), {:init, zero_th}})
receive do
{:ok, message} -> IO.puts message
end
test_sensor_repeat(pid)
end
ここからが elixir のおもろいところと言うか、ゆるいところというか、変数の宣言をしないので、呼び出し方でこういう使い方ができます。
iex(1)> c "spawn7.ex"
[Spawn7]
iex(2)> Spawn7.test_sensor(0)
Initial temperature is 0
Next temperature is 0
Next temperature is -7
Next temperature is -1
Next temperature is 2
Next temperature is 1
Next temperature is 7
Next temperature is 7
Next temperature is 15
Next temperature is 15
Next temperature is 7
:ok
iex(3)>
これ↑↑↑↑↑は初期化で整数の 0 を使ってます。続く値も全部整数になります。
iex(3)> Spawn7.test_sensor(0.0)
Initial temperature is 0.0
Next temperature is 0.0
Next temperature is 2.0
Next temperature is -3.0
Next temperature is -3.0
Next temperature is -10.0
Next temperature is -19.0
Next temperature is -15.0
Next temperature is -9.0
Next temperature is -9.0
Next temperature is -9.0
:ok
iex(4)>
これ↑↑↑↑↑は初期化で浮動小数の 0.0 を使ってます。続く値も全部浮動小数になります。
ちょっとおもしろいですが、ちゃんとわかってないとバグの原因になりそうですね。
整理しまくったらこんなにシンプルになった
おんなじ関数を繰り返さず test_sensor への引数で指定するようにしました。ついでにいろいろと整理してみたらこんなにシンプルになりました。
- Enum で回数指定をするようにした
- :init で呼ぶときの返り値は初期化の結果として、:next は現状の値を先に返して、新しい値は再帰呼び出しの引数とした
- その他、デバッグプリントやら、おかしくなったときに脱出する部分を削除した
defmodule Spawn8 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do mes -> mes
end
end
def test_sensor(zero_th, num) do
pid = spawn(Spawn8, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
結果はリストになりますが、これも同じ様に動きます。
iex(1)> c "spawn8.ex"
[Spawn8]
iex(2)> Spawn8.test_sensor(0, 10)
[0, 0, 1, 0, -3, -3, 0, 0, 2, -1]
iex(3)> Spawn8.test_sensor(0.0, 10)
[0.0, -1.0, -4.0, -4.0, -4.0, -7.0, -5.0, -2.0, -3.0, -1.0]
iex(4)> Spawn8.test_sensor(10.0, 10)
[10.0, 10.0, 10.0, 11.0, 11.0, 11.0, 14.0, 14.0, 14.0, 14.0]
iex(5)>
今日はここまで。
もくもく会でリモートで発表しませんでしたが、こんな事やってました。みなさん、お疲れ様でした。