7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

はじめてなElixir(17)軽くAgentを試すつもりがハマる

Last updated at Posted at 2018-10-06

プログラミングElixirを読んでいるとOTPであれこれした後にこんなことが書いてあります。

しかし時には、中間のものがほしくなる。

spawn シリーズでは舞台裏が見えすぎるし、かと言ってGenServerでは何でも出来すぎて大変だし、その「中間のものがほしくなる」ということです。
はじめてなElixir(15)はじめてなElixir(16)でGenServerで楽しめましたし、OTPにつながる何かを感じてエキサイティングでした。しかし、その先が重たそうです。先に進む前にもう少し軽いプロセスで遊びたい。プログラミングElixirを読み進んでいくと第19章にタスクとエージェントという章があるので、それをやってみます。

タスクとエージェント

プログラミングElixir本、この章はなんだか手抜き感があって、読者に一言伝えておこうぐらいな短い章です。そのせいでちょっとわかりにくく、微妙なハマり方をしてしまいました。

Agent として書き直してみる

チャチャっと遊ぶのにプロセスの動作を簡単にしてあります。

  • プロセスの起動時は初期状態として [0] を持つ
  • データの更新リクエスト (cast, update) で先頭要素を+1した値でリストを伸ばす
    • 要は [n, ..., 1, 0] ってリストになる
  • 現在の状態を得る (get) ことができる

あと pid を持ち回るのもアレなので名前を付けられるようにしました。

defmodule Temp11 do
  def start_link(agent) do
    Agent.start_link(fn -> [0] end, name: agent)
  end

  def cast(agent) do
    Agent.cast(agent, Temp11.succ())
  end

  def update(agent) do
    Agent.update(agent, Temp11.succ())
  end

  def get(agent) do
    Agent.get(agent, &(&1))
  end

  def succ() do
    fn(l) -> [hd(l) +1 | l] end
  end
end

これを実行するとこうなります。

iex(1)> Temp11.start_link(Bar)
{:ok, #PID<0.106.0>}
iex(2)> Temp11.get(Bar)
[0]
iex(3)> Temp11.cast(Bar)
:ok
iex(4)> Temp11.get(Bar) 
[1, 0]
iex(5)> Temp11.update(Bar)
:ok
iex(6)> Temp11.get(Bar)   
[2, 1, 0]
iex(7)> 

とまあだんだんとリストが長くなっていきます。名前を付けられるようにしたので別のエージェント(プロセス)も動かしてみましょう。

iex(7)> Temp11.start_link(Foo)
{:ok, #PID<0.113.0>}
iex(8)> Temp11.get(Foo)       
[0]
iex(9)> Temp11.cast(Foo)
:ok
iex(10)> Temp11.cast(Foo)
:ok
iex(11)> Temp11.cast(Foo)
:ok
iex(12)> Temp11.cast(Foo)
:ok
iex(13)> Temp11.get(Foo)       
[4, 3, 2, 1, 0]
iex(14)> Temp11.get(Bar)
[2, 1, 0]
iex(15)> 

とこのように Bar エージェントと Foo エージェントを独立に名前で扱えます。

コールバックの書き方に注意

これだけ見ると実に簡単そうですが、今回もしっかりハマりました。上で cast や update の引数に「状態に対してどんな扱いをするか」関数を記述してます。こんな風に書いても良いです。
Agent.cast(agent, fn(l)->[hd(l)+1 | l] end)
とか
Agent.cast(agent, fn([h | t]) -> [h+1 | [h | t]] end)
とか
Agent.cast(agent, &([hd(&1)+1 | &1]))
とか.

最終的なプログラムでは関数 succ を別に定義して関数名で渡してます。
Agent.cast(agent, Temp11.succ())
これで一番ハマりました。最初は succ 関数をこう書いていたのです。

  def succ(l) do
    [hd(l)+1 | l]
  end

はい、これ succ/1 関数です。動かないんです、これでは。「succ/0 がナイ」って怒られるんです。上の匿名関数で書いたのは全部引数を1つ取ります。この succ/1 じゃだめなんでしょうか。考えに考えた挙げ句、以下の関数にしました。これは「匿名関数を渡す関数(のつもり)」です。結局、これでうまく動きました。かなりハマりました。

  def succ() do
    fn(l) -> [hd(l)+1 | l] end
  end

cast と update とで何が違うのか

実行例では cast と update で差がありません。どちらも状態を次の状態に進めます。これの差を見つけるのも苦労しました。GenServer の cast/call との類推で、「Agent での計算に時間がかかるときに、cast はすぐ戻ってきて、update は計算結果が出るのを待つのでは」と思って :timer.sleep を入れてみても動作が同じです。差が出るのは以下の場合でした。

iex(1)>  Temp11.start_link(Foo)
{:ok, #PID<0.106.0>}
iex(2)> Temp11.cast(Foo)
:ok
iex(3)> Temp11.update(Foo)
:ok
iex(4)> Temp11.cast(Bar)
:ok
iex(5)> Temp11.update(Bar)
** (exit) exited in: GenServer.call(Bar, {:update, #Function<2.9658421/1 in Temp11.succ/0>}, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir) lib/gen_server.ex:914: GenServer.call/3
iex(5)> 

素直なのが update ですね。エージェント Foo に対しては普通に動いて、ありもしないエージェント Bar に対してはエラーします。驚くのが cast で、エージェントがあろうがなかろうが :ok を返します。

マニュアルをよく読んでみる

とまあいろいろとハマりました。まあよく毎回ハマります。よくよくリファレンスマニュアル Elixir 1.7.3 を読むと書いてあるのですよね。これ。

cast/2 と update/2 には匿名関数を与える

Elixir 1.7.3 Agent cast/2 には与える関数についてこう書いてあります。

The function fun is sent to the agent which invokes the function passing the agent state. The return value of fun becomes the new state of the agent.

これはフムフムですね。一方 Elixir 1.7.3 Agent cast/4 にはこう書いてあります。

Same as cast/2 but a module, function, and arguments are expected instead of an anonymous function. The state is added as first argument to the given list of arguments.

え? instead of an anonymous function だって? あのなぁ、それを cast/2 に書いてといてくれよ。

2018.10.07追記
Summary の update/3 を見ると
update(agent, fun, timeout \\ 5000)
Updates the agent state via the given anonymous function
とこちらには明示的に anonymous function と書いてありました。cast/2 に加えて update/3 も見ておけば分かってたところでした。

cast と update の違い

これは Summary を読んでると完全には分かりません。

cast(agent, module, fun, args)
Performs a cast (fire and forget) operation on the agent state

update(agent, fun, timeout \\ 5000)
Updates the agent state via the given anonymous function

ここで分かるのは update だと第3引数があって5000がデフォルト値になってるということです。これを下にある Function の詳しい説明で見るとさらに違いが分かります。cast にはこう書いてあります。

Note that cast returns :ok immediately, regardless of whether agent (or the node it should live on) exists.

agentがあろうがなかろうが:okを直ちに返す」とありあすね。また update にはこう書いてあります。

timeout is an integer greater than zero which specifies how many milliseconds are allowed before the agent executes the function and returns the result value, or the atom :infinity to wait indefinitely. If no result is received within the specified time, the function call fails and the caller exits.

cast は何があろうが(エージェントがなかろうが)すぐに返ってくるのに対して、update はすぐに返ってこないときに備えてタイムアウトするようになってます。

とまあ、ドキュメントに書いてあることはあるのでした。「これを先に読んでおけば…」という気分と「全部わかった今だからこそ、読んで意味がわかるよなぁ…」という気分と。さて風呂入って寝るか。

参考文献

プログラミングElixir
Elixir 1.7.3
Elixir リファレンス
最後の日本語版は最新版には対応してませんが、Process 関連はあまり変わってないんじゃないかと思います(確証なし)。

7
4
1

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
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?