どうも、靖宗です。
地味に第n章の箇所を漢数字からアラビア数字に変更しました。いちいち変換するのめんどくさい・・・
- The trouble with state
- Agents
- Test setup with ExUnit callbacks
- Other agent actions
- Client/Server in agents
お次はAgent。たぶんMix and OTP編の全ての章に該当すると思うんですが、第1章を踏まえた上での内容になっとります。
あとProcessesも読んどいた方がいいよってあるので第十一章も必読かもしれません。
この章ではKV.Bucket
というモジュールを作成していくようです。KV自体がモジュールじゃないの?
The trouble with state
Elixirは、というか関数型言語全般?は変数などが不変の言語なので出したり入れたりする入れ物の場所を作ってあげないと行けないそうで。
一見不便に見えるんですが、おそらくこの辺をしっかり設計してると堅牢なシステムが作れるんでしょうね。
それはさておき、入れ物を実現するにはElixirでは2種類の方法が考えられます。
- Processes
- ETS (Erlang Term Storage) まだやってない
ETSはあとの章で言及があるとして、プロセスは別プロセスとして起動してメッセージでやりとりするアレ(State?)ですね。
ただいちいちプロセスで云々やることは少ないそうで、ElixirやOTPでは下記の3つが採用されるそうです。
- Agent シンプルな状態管理ラッパー?
- GenServer 状態をカプセル化するプロセス?同期非同期使えてコードを再ロードできる。しらんけど。
- Task 非同期の何か。名前的に並列処理のときとかに使いそう。
たぶん全部プロセスを拡張したようなやつ。
とりあえず読み進めましょう。
Agents
まずはAgenst(エージェント?)。プロセスのStateを再現したいならたぶんこれ。iex -S mix
でiexを起動してサンプルを打ってみます。
iex(1)> {:ok, agent} = Agent.start_link fn -> [] end {:ok, #PID<0.129.0>} iex(2)> Agent.update(agent, fn list -> ["eggs" | list] end) :ok iex(3)> Agent.get(agent, fn list -> list end) ["eggs"] iex(4)> Agent.stop(agent) :ok
だいたいプロセスのところでやったStateと同じ様な感じなんですが、元から用意されてるんですね。
いちいちsendとかする代わりにAgentモジュールでやりとりする感じです。
Agent.update
などは引数に関数を与えてますが、パイプラインを形成してるイメージでしょうか?(引数->結果をどんどん紡いでいく)
Agentを利用してKV.Bucket
を実装していきますが、まずはKV.BucketTest
を書いていきます。
場所はtest/kv/bucket_test.exs
に保存します。(モジュール名/ファイル名
が一般的?)
defmodule KV.BucketTest do use ExUnit.Case, async: true test "stores values by key" do {:ok, bucket} = KV.Bucket.start_link([]) # KV.Bucketをstart_link/1で呼び出す(プロセス生成?) assert KV.Bucket.get(bucket, "milk") == nil # 何も登録してないのでキーで呼び出しても何も返ってこない KV.Bucket.put(bucket, "milk", 3) # "milk"というキーに3という値を登録 assert KV.Bucket.get(bucket, "milk") == 3 # 登録したキーで3が呼び出せてるかテスト end end
use ExUnit.Case
にasync
というオプションがついてますが、どうやらテストを並列で実行するオプションだそうです。
基本付けてた方が早く終わりそうなもんですが、ファイルを触ったりデータベースを触ったりするテストの場合は競合したりするのでこのオプションは使わない方が良さげです。
ここまででmix test
してみると、失敗しますが一応テストファイルは認識しているようです。
早速本体のKV.Bucket
を書いていきます。テスト同様lib/kv/bucket.ex
に書いていきます。
defmodule KV.Bucket do use Agent # Agentモジュールを利用する @doc """ Starts a new bucket. """ def start_link(_opts) do Agent.start_link(fn -> %{} end) # 初期値に空のマップを登録してエージェントを立ち上げる end @doc """ Gets a value from the `bucket` by `key`. """ def get(bucket, key) do Agent.get(bucket, &Map.get(&1, key)) # Agent.getを利用する。 # イメージとしては投げる関数はパイプラインに乗る? Agentの保持してるマップ |> &Map.get(&1, key) end @doc """ Puts the `value` for the given `key` in the `bucket`. """ def put(bucket, key, value) do Agent.update(bucket, &Map.put(&1, key, value)) # Agent.putを利用する。 # get同様イメージとしてはパイプライン Agentの保持してるマップ |> &Map.put(&1, key, value) ←コレの返値が保持する状態になる end end
&
などは関数キャプチャ。第八章を参照。
たぶん思ってた機能は実装できたのでテストを回してみる。
PS > mix test Compiling 1 file (.ex) ... Finished in 0.03 seconds 1 doctest, 2 tests, 0 failures Randomized with seed 614000
ウレシイ!
Test setup with ExUnit callbacks
KV.Bucket
の機能を追加していく前に、テストをもうチョイ綺麗に書く準備をします。
今のままだとテスト毎に{:ok, bucket} = KV.Bucket.start_link([])
してうにゃうにゃしないと行けないのでBucketのエージェントを立ち上げるところは共通化したいです。
そこでExUnit
の機能callbackという機能を用いて共通のヘッダみたいなのを作って行きます。
defmodule KV.BucketTest do use ExUnit.Case, async: true # ここからsetup/1マクロのcallback setup do {:ok, bucket} = KV.Bucket.start_link([]) %{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 end
callbackで値を返しておけばtestの時にパターンマッチで拾ってこれるようです。
今はまねするだけでよさそうですが、詳しくはドキュメントを読むのが良いと思います。
Other agent actions
さて、元のKV.Bucket
に機能を追加していきます。
CRUDで言うところのCRUまでは出来てると思うので、あとは削除です。
Agentには削除にあたる機能が直接備わっているのではなく、Agent.get_and_update/2
(キーを元に取得して更新する)を利用して削除を実装するのがよさそうです。
@doc """ Deletes `key` from `bucket`. Returns the current value of `key`, if `key` exists. """ def delete(bucket, key) do Agent.get_and_update(bucket, &Map.pop(&1, key)) # Map.pop(マップ, キー)でマップからキーを削除した物を返す? end
削除が実装できました。
削除の動作もテストに書いておきます。以下はテストファイルに追記しました。
test "delete value by key", %{bucket: bucket} do KV.Bucket.put(bucket, "milk", 3) assert KV.Bucket.delete(bucket, "milk") == 3 # 削除したときに値が返ってくるので一応 assert KV.Bucket.get(bucket, "milk") == nil # 削除したキーが空であることを確認 end
テスト実行!
PS > mix test Compiling 1 file (.ex) .... Finished in 0.03 seconds 1 doctest, 3 tests, 0 failures Randomized with seed 929000
KVモジュールのハローワールド、KV.Bucketの2種類のテスト合計3つのテストが問題無く通ってます。
Client/Server in agents
次の章に行く前に、エージェントに於けるクライアント/サーバーの関係性を説明してくれてます。
例として先ほどのKVBucket.delete/2
を改良していきます。
def delete(bucket, key) do Agent.get_and_update(bucket, fn dict -> Map.pop(dict, key) end) end
機能としては全く一緒。
エージェントとの関係性を強調するためにちょっと分かりやすく変更しただけだと思います。
ここではエージェントは別プロセスで実行されており、メッセージのやりとりで状態管理の機能が実装されているはずです。
ですので、ここでメッセージを送る(最初によこせ!とか更新しろ!とか言う)プロセスをクライアント、そのメッセージを受けるプロセスをサーバーとしましょう、という決め毎でしょうか。
相対関係をはっきりさせるために定義してるだけだと思いますので、そこまで深く考えなくてもいい気はします。(イメージ的にもたしかにクライアントとサーバーの関係性だし)
先ほどのコードをより強調して書く。
def delete(bucket, key) do Process.sleep(1000) # クライアントで1000ミリ秒待つ Agent.get_and_update(bucket, fn dict -> Process.sleep(1000) # サーバーで1000ミリ秒待つ Map.pop(dict, key) end) end
同期、非同期などでハマらないようにするためにも、このクライアント/サーバーの関係性は意識したいところです。