MixとOTP 02: Agent
Tags: elixir, webdev, tutorial, programming
Published: 2019-03-22 05:21:47

---
title: MixとOTP 02: Agent
published: true
description: Elixir公式サイトの許諾を得て「Agent」の解説にもとづき、加筆補正を加えて、ElixirのAgentとExUnitによるそのテストについてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Agent](https://elixir-lang.org/getting-started/mix-otp/agent.html)」の解説にもとづき、加筆補正を加えて、Elixirの`Agent`と`ExUnit`によるそのテストについてご説明します。

練習用のコードは、モジュール`KV`に子モジュールを加えるかたちで進めます。

```terminal
$ mix new kv --module KV
```

> 「MixとOTP」シリーズをとおしてつくられるアプリケーションは[`josevalim/kv_umbrella`](https://github.com/josevalim/kv_umbrella)です。

プロセスを扱いますので、必要に応じて「[Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)」をご参照ください。


# 状態の操作
ElixirとOTPでは、つぎのような抽象化された機能が使えます。

- [`Agent`](https://hexdocs.pm/elixir/Agent.html): 状態を包む単純なラッパー。
- [`GenServer`](https://hexdocs.pm/elixir/GenServer.html): 状態をカプセル化する「汎用サーバー」([プロセス](https://dev.to/gumi/elixir-11--2mia))。同期と非同期の呼び出しやコードの再読み込みなどに対応します。
- [`Task`](https://hexdocs.pm/elixir/Task.html): 非同期の処理単位。プロセスを生成したり、あとから結果を得ることもあります。

これらの機能はプロセス上に実装されています。そして、VMに備わる`send`や`receive`、`spawn`、`link`などの基本的な操作を用いているのです。


# Agent
`Agent`は、状態を包む単純なラッパーです。プロセスから得たいのが状態だけでしたら、もっとも適しています。`iex`のセッションをプロジェクト内で始めましょう。

```terminal
$ iex -S mix
```

まず、新たな`Agent`を開始して、空のリスト`[]`の状態で初期化します。つぎに、`Agent`を更新して、新たな項目はリストのヘッドに加えます。そのうえで、更新されたリストを値として得ています。最後に`Agent`は停止して、プロセスを終了させます。

```elixir
iex> {:ok, agent} = Agent.start_link(fn -> [] end)
{:ok, #PID<0.138.0>}
iex> Agent.update(agent, fn list -> ["eggs" | list] end)
:ok
iex> Agent.get(agent, fn list -> list end)
["eggs"]
iex> Agent.stop(agent)
:ok
```

- [`Agent.start_link/2`](https://hexdocs.pm/elixir/Agent.html#start_link/2): 現在のプロセスにリンクした`Agent`を引数の関数でつくります。
- [`Agent.update/3`](https://hexdocs.pm/elixir/Agent.html#update/3): 第1引数の`Agent`の状態を、第2引数の関数で更新します。関数が引数に受け取るのは、`Agent`の現在の状態です。
- [`Agent.get/3`](https://hexdocs.pm/elixir/Agent.html#get/3): 第1引数の`Agent`から値を、第2引数の関数で受け取ります。
- [`Agent.stop/3`](https://hexdocs.pm/elixir/Agent.html#stop/3): `Agent`を同期的に停止します。

`Agent`をモジュールに実装しましょう。新たにつくるモジュールは`KV.Bucket`です。けれど、実装より先にテストを書くことにします。プロジェクトの中にテストファイル`test/kv/bucket_test.exs`をつくり、記述するのは以下のコードです(テストファイルの拡張子は`.exs`とします)。

テスト([`test/3`](https://hexdocs.pm/ex_unit/ExUnit.Case.html#test/3))はまず、モジュール`KV.Bucket`の`start_link/1`に空のリストを渡して呼び出します。つぎに、`get/2`と`put/3`を順に呼び、それぞれ[`assert/1`](https://hexdocs.pm/ex_unit/ExUnit.Assertions.html#assert/1)で結果が確かめられます。

[`ExUnit.Case`](https://hexdocs.pm/ex_unit/ExUnit.Case.html)にオプション`async: true`を渡していることにご注意ください。このオプションにより、テストは非同期で行われます。つまり、マルチコアのマシン上で、他の非同期(`async: true`)のテストケースと並行して実行されるのです。テストスイートを高速化するのにとても効果があります。ただし、非同期の設定は、グローバルな値に依存したり、変更したりしない場合にのみ用いてください。たとえば、テストのときファイルシステムに書き込んだり、データベースを参照する場合には、同期処理(`:async`は使わない)にしないとテスト間の競合状態が招かれるからです。


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

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

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

このテストは今はもちろん失敗します。まだ、モジュール`KV.Bucket`そのものを書いていないからです(module KV.Bucket is not available)。

```terminal
$ mix test test/kv/bucket_test.exs


  1) test stores values by key (KV.BucketTest)
     test/kv/bucket_test.exs:4
     ** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not
 available)
     code: {:ok, bucket} = KV.Bucket.start_link([])
     stacktrace:
       KV.Bucket.start_link([])
       test/kv/bucket_test.exs:5: (test)



Finished in 0.02 seconds
1 test, 1 failure

Randomized with seed 980520
```

`KV.Bucket`モジュールは、新たなファイル`lib/kv/bucket.ex`に定めます。コードは以下のとおりです。まず、[`use/2`](https://hexdocs.pm/elixir/Kernel.html#use/2)で、コンテキストにモジュール`Agent`を使います。

つぎに、`start_link/1`関数が`Agent`を開始します。`start_link/1`にはオプションとしてリストを受け取るのが通例です。今のところ、オプションは定めておくだけで使いません。関数本体からは`Agent.start_link/2`を呼び出して、引数には無名関数が渡され、`Agent`の初期状態を返します。

`Agent`にはマップでキーと値を納めます。`get/2`と`put/3`は`Agent`のAPIでマップの値を操作し、引数の関数にはキャプチャ演算子`&`が用いられています(「[Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)」の「関数のキャプチャ」参照)。

```elixir
defmodule KV.Bucket do
  use Agent
  @doc """
  新たな`Bbucket`をつくる。
  """
  def start_link(_opts) do
    Agent.start_link(fn -> %{} end)
  end
  @doc """
  `bucket`から`key`で値を得る。
  """
  def get(bucket, key) do
    Agent.get(bucket, &Map.get(&1, key))
  end
  @doc """
  `bucket`の`key`に`value`を与える。
  """
  def put(bucket, key, value) do
    Agent.update(bucket, &Map.put(&1, key, value))
  end
end
```

`KV.Bucket`モジュールが定められ、使われる関数も実装しましたので、テストは成功します。

```terminal
$ mix test test/kv/bucket_test.exs
Compiling 1 file (.ex)
.

Finished in 0.02 seconds
1 test, 0 failures

Randomized with seed 313891
```

# ExUnitのコールバックによるテストの設定
`Agent`を使ったモジュールのテストでは、予め`Agent`を起ち上げておかなければなりません。テストごとに起動せずに済むように、`ExUnit`にはコールバックが備わっています。コールバックを定めるのは[`setup/1`マクロ](https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#setup/1)です。`setup/1`のコールバックは各テストの前に、テストと同じプロセスで実行されます。

テストに渡しているのは、コールバックから返されるマップのPIDです。このときテストのコンテキストが用いられます。コールバックから`%{bucket: bucket}`が返されると、`ExUnit`はテストコンテキストにマップを取り込みます。テストコンテキストもマップですので、パターンマッチングを使って、テストの中の`%{bucket: bucket}`が参照できるのです。

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

  setup do  # setupを追加
    {:ok, bucket} = KV.Bucket.start_link([])
    %{bucket: bucket}
  end

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

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

さらに詳しくは「[`ExUnit.Case`](https://hexdocs.pm/ex_unit/ExUnit.Case.html)」をお読みください。また、コールバックについては「[`ExUnit.Callbacks`](https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html)」に解説されています。


# Agentのその他のアクション
`Agent.get/3`で値を得て、`Agent.update/3`により状態を更新するほかに、ふたつの処理をひとつの関数の呼び出しで行うこともできます。それが[`Agent.get_and_update/3`](https://hexdocs.pm/elixir/Agent.html#get_and_update/3)です。現在の値を返すとともに、状態は更新されます。この関数により`Agent`からキーを除くのが、以下に加えた`KV.Bucket.delete/2`関数です。

```elixir
@doc """
`bucket`から`key`を除きます。`key`が存在していたら、その値を返します。
"""
def delete(bucket, key) do
  Agent.get_and_update(bucket, &Map.pop(&1, key))
end
```

練習として、新たな関数`KV.Bucket.delete/2`のテストを加えてみましょう。`Agent`について詳しくは、モジュール「[`Agent`](https://hexdocs.pm/elixir/Agent.html)」の解説をお読みください。


# Agentのクライアント/サーバー
`Agent`におけるクライアントとサーバーの区別について触れておきます。前掲`KV.Bucket.delete/2`を例として使いますので、手が加えやすいように関数はキャプチャせず、`Map.pop/3`を呼び出します。

```elixir
def delete(bucket, key) do
  Agent.get_and_update(bucket, fn dict ->
    Map.pop(dict, key)
  end)
end
```

`Agent`に渡した関数の中で行う処理は、すべてその`Agent`のプロセスで扱われます。`Agent`のプロセスはメッセージを受け渡すので、このプロセスはサーバーと呼ばれます。引数の関数の外で扱われるものがクライアントです。ふたつの区別は重要です。負荷の高い処理があるとき、実行をクライアントにするかサーバーにするか考えなければなりません。

時間のかかる処理がサーバーで行われると、そのサーバーへの他のリクエストは処理が終わるのを待つことになります。すると、クライアントによってはタイムアウトが生じるかもしれないのです。

```elixir
def delete(bucket, key) do
  Process.sleep(1000) # クライアントを待たせる
  Agent.get_and_update(bucket, fn dict ->
    Process.sleep(1000) # サーバーを待たせる
    Map.pop(dict, key)
  end)
end
```


#### MixとOTPもくじ
- [MixとOTP 01: Mixを使ってみる](https://dev.to/gumi/mixotp-01-mix-dg1)
- MixとOTP 02: Agent
- [MixとOTP 03: GenServer](https://dev.to/gumi/mixotp-03-genserver-2jhn)
- [MixとOTP 04: スーパーバイザーとアプリケーション](https://dev.to/gumi/mix-otp-04-bj0)
- [MixとOTP 05: ダイナミックスーパーバイザー](https://dev.to/gumi/mix-otp-05-l3p)
- [MixとOTP 06: ETS](https://dev.to/gumi/mix-otp-06-ets-ol3)
- [MixとOTP 07: 依存関係とアンブレラプロジェクト](https://dev.to/gumi/mix-otp-07-1p5k)
- [MixとOTP 08: Taskとgen_tcp](https://dev.to/gumi/mix-otp-08-gentcp-5120)
- [MixとOTP 09: DocTestとwithのパターンマッチング](https://dev.to/gumi/mix-otp-09-doctest-with-533g)
- [MixとOTP 10: 分散処理のTaskと設定](https://dev.to/gumi/mix-otp-10-3i31)