10
2

More than 5 years have passed since last update.

はじめてなElixir(11) 並行プロセスであそぶ

Last updated at Posted at 2018-09-28

はじめに

今日は fukuoka.ex のもくもく会です。fukuoka.ex のもくもく会どころか「もくもく」と名前がついている会合すら初めてです。ここでやったことをそのまま貼り付けてみます。

これまで
はじめてなElixir(9) 有限状態機械を作ってみる
はじめてなElixir(10) 小水力発電所の状態遷移を表現してみる
と、状態機械で遊んできましたが、もうちょっと先に行ってみようと思います。そのために今日は生プロセスにチャレンジしてみます。

とりあえず教科書のまま

まずは「プログラミングElixir」14章の先頭のママで遊んでみます。

spawn4.ex
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 で受け取るメッセージでパターンマッチできそうなので、そこで条件分岐させてみます。

spawn5.ex
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)> 

状態を持たせてみちゃおうかなぁ

いいのかなぁ、いいのかなぁ、それももくもく会でやっちゃってもいいのかなぁ。良い子は気をつけましょう。

明示的な状態ではもたせられない

なにかグローバルな変数を持っててそれを持ち回るってのはできないのか、私が知らないのか、とにかくできそうにないです。そういう言語ですから。んで、どうするかというと、今回は関数の引数で持ってみるというのをやってみます。

spawn6.ex
  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 で手で毎回打ってると大変なので、プログラムを組んでしまいます。

spawn6.ex(続き)
  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)
としてみます。

spawn7.ex
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

ちょっとずつ値が変わるはずです。テストプログラムをこんな感じで。

spawn7.ex(続き)
  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 は現状の値を先に返して、新しい値は再帰呼び出しの引数とした
  • その他、デバッグプリントやら、おかしくなったときに脱出する部分を削除した
spawn8.ex
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)> 

今日はここまで。
もくもく会でリモートで発表しませんでしたが、こんな事やってました。みなさん、お疲れ様でした。

10
2
2

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