スポンサーリンク

2015年8月21日

[Elixir]基本的なAgentの使い方を習得する

とある錬金術師の万能薬(Elixir)

Goal

基本的なAgentの使い方を習得する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4

Wait a minute

プロセスの次は、Agentをやりましょう。
Getting StartのAgentをやった感想としては・・・
ソースコード見ろって感じな内容ですね。
プロセスの前提知識が必要らしいので、プロセスの基本を習得を読んでおくと理解が早いかもしれません。
Elixirはイミュータブル(作成後にその状態を変えることのできないこと)な言語とのこと。
だから、状態(State)を変えるためには、プロセスかETS(Erlang Term Storage)を利用する必要がある。
(ETSは余り利用される機会はないらしいが)
しかし、プロセスをそのまま使うと色々と面倒である。
だから・・・ElixirとOTPで抽象化されている機能が提供されている。
  • Agent ・・・ ステート周りのラッパー
  • GenServer ・・・ プロセスをカプセル化、同期通信、非同期通信、コードの再読込みをサポート
  • GenEvent ・・・ イベントの共有化や複数のハンドラー管理
  • Task ・・・ プロセスを発行、後から呼び出しができる(非同期処理)

Index

Agent
|> Preparation
|> KV.Bucket
|> Extra

Preparation

Agentを使い、異なるプロセス間で状態を保持します。
その例として、キーバリューストアの読込みや変更を
異なるプロセスで処理を行う簡単なアプリケーションを作成します。
プロジェクトを作成します。
>cd プロジェクト作成ディレクトリ
>mix new kv
>cd kv
>mix test
おそらく・・・
kv = key value

KV.Bucket

テストケースとモジュールの二つを作成します。

ファイル: test/bucket_test.exs

defmodule KV.BucketTest do
  use ExUnit.Case, async: true

  setup do
    {:ok, bucket} = KV.Bucket.start_link
    {:ok, bucket: bucket}
  end

  test "stores values by key", %{bucket: bucket} do
    assert KV.Bucket.get(bucket, "milk") == nil

    KV.Bucket.put(bucket, "milk", 3)
    assert KV.Bucket.get(bucket, "milk") == 3
  end

  test "deletes key from bucket", %{bucket: bucket} do
    assert KV.Bucket.delete(bucket, "milk") == nil

    KV.Bucket.put(bucket, "milk", 3)
    assert KV.Bucket.delete(bucket, "milk") == 3
  end
end
Description:
:asyncは、テストが他のテストケースも含めて一斉に実行されるオプション。
注意点としては・・・
有効にしている間は、グローバルな値(ファイルシステム、プロセスの登録、データベースの操作など)の参照/変更ができないこと。
上記の操作を行う場合は、テスト間の競合をさけるため非同期させないようにすること。
Description:
テストにおけるコールバック処理の一つ。
テストで毎回必要な同一の処理を記述しておくと便利。
setup do
  {:ok, bucket} = KV.Bucket.start_link
  {:ok, bucket: bucket}
end

test "stores values by key", %{bucket: bucket} do
  ...
end
テストはあまり得意でないので、詳しく書きません。
詳しく知りたい方は、ドキュメントの方を見て下さい。
ドキュメント: hexdocs - v1.0.5 Elixir ExUnit.Case
ドキュメント: hexdocs - v1.0.5 Elixir ExUnit.Callbacks
ドキュメント: hexdocs - v1.0.5 Elixir ExUnit.Assertions

ファイル: lib/bucket.ex

defmodule KV.Bucket do
  @doc """
  Starts a new bucket.
  """
  def start_link do
    Agent.start_link(fn -> HashDict.new end)
  end

  @doc """
  Gets a value from the `bucket` by `key`.
  """
  def get(bucket, key) do
    Agent.get(bucket, &HashDict.get(&1, key))
  end

  @doc """
  Puts the `value` for the given `key` in the `bucket`.
  """
  def put(bucket, key, value) do
    Agent.update(bucket, &HashDict.put(&1, key, value))
  end

  @doc """
  Deletes `key` from `bucket`.

  Returns the current value of `key`, if `key` exists.
  """
  def delete(bucket, key) do
    Agent.get_and_update(bucket, &HashDict.pop(&1, key))
  end
end
Description:
指定された関数で、現在のプロセスにリンクされているAgentを起動します。
オプションのデフォルト値は空リスト。

Agent.get/3

与えられた関数を介してAgentの値を取得します。
タイムアウトが指定できる。デフォルト値は5000。

Agent.update/3

Agentの状態を更新します。
タイムアウトが指定できる。デフォルト値は5000。

Agent.get_and_update

エージェントの状態を一回の操作で更新/取得します。
タイムアウトが指定できる。デフォルト値は5000。
詳しい説明はドキュメントを見て下さい。
ドキュメント: hexdocs - v1.0.5 Elixir Agent

Extra

start_link/2の引数の意味がよく分からなかったので、
Sapporo.beam@niku氏に聞いてみました。
質問: 「Agent.start_link(fn -> “” end)」で引数の無名関数は何をやっているのでしょうか?
解答: 初期値の設定。
iex> {:ok, agent} = Agent.start_link(fn -> "" end)
{:ok, #PID<0.61.0>}
iex> Agent.get(agent, fn state -> state end)
""
iex> {:ok, agent} = Agent.start_link(fn -> 2 end) 
{:ok, #PID<0.65.0>}
iex> Agent.get(agent, fn state -> state end)     
2
@niku氏、ありがとうございました。

Speaking to oneself

クライアントとサーバの区別をしっかりつけましょうって、
注意点が書いてあったが・・・いまいちピンとこないな。
処理の量や大きさによって負荷の軽減を考えるねぇ。
言われれば、まぁ何となくは想像できるけど・・・
まぁやっていく内に分かることでしょう。(楽観)

Bibliography

人気の投稿