AgentMap
AgentMap
is a GenServer
that holds Map
and provides concurrent access
via Agent
API for operations made on different keys. Basically, it can be
used as a cache, memoization and computational framework or, sometimes, as a
GenServer
replacement.
AgentMap
can be seen as a Map
, each value of that is an Agent
. When a
callback that change state (see update/3
, get_and_update/3
, cast/3
and
derivatives) comes in, special temporary process (called “worker”) is created.
That process holds queue of callbacks for corresponding key. AgentMap
respects order in which callbacks arrives and supports transactions —
operations that simultaniously change group of values.
Module API is in fact a copy of the Agent
’s and Map
’s modules. Special
struct that allows to use Enum
module and []
operator can be created via
new/1
function.
Examples
Let’s create an accounting.
defmodule Account do
use AgentMap
def start_link() do
AgentMap.start_link(name: __MODULE__)
end
@doc """
Returns `{:ok, balance}` for account or `:error` if account
is unknown.
"""
def balance(account), do: AgentMap.fetch(__MODULE__, account)
@doc """
Withdraw. Returns `{:ok, new_amount}` or `:error`.
"""
def withdraw(account, amount) do
AgentMap.get_and_update(__MODULE__, account, fn
nil -> # no such account
{:error} # (!) returning {:error, nil} would create key with nil value
balance when balance > amount ->
{{:ok, balance-amount}, balance-amount}
_ ->
{:error}
end)
end
@doc """
Deposit. Returns `{:ok, new_amount}` or `:error`.
"""
def deposit(account, amount) do
AgentMap.get_and_update(__MODULE__, account, fn
nil ->
{:error}
balance ->
{{:ok, balance + amount}, balance + amount}
end)
end
@doc """
Trasfer money. Returns `:ok` or `:error`.
"""
def transfer(from, to, amount) do
# Transaction call.
AgentMap.get_and_update(__MODULE__, fn
[nil, _] -> {:error}
[_, nil] -> {:error}
[b1, b2] when b1 >= amount ->
{:ok, [b1 - amount, b2 + amount]}
_ -> {:error}
end, [from, to])
end
@doc """
Close account. Returns `:ok` if account exists or
`:error` in other case.
"""
def close(account) do
if AgentMap.has_key?(__MODULE__, account do
AgentMap.delete(__MODULE__, account)
:ok
else
:error
end)
end
@doc """
Open account. Returns `:error` if account exists or
`:ok` in other case.
"""
def open(account) do
AgentMap.get_and_update(__MODULE__, account, fn
nil -> {:ok, 0} # set balance to 0, while returning :ok
_ -> {:error} # return :error, do not change balance
end)
end
end
Memoization example.
defmodule Memo do
use AgentMap
def start_link() do
AgentMap.start_link(name: __MODULE__)
end
def stop(), do: AgentMap.stop(__MODULE__)
@doc """
If `{task, arg}` key is known — return it, else, invoke given `fun` as
a Task, writing result under `{task, arg}`.
"""
def calc(task, arg, fun) do
AgentMap.get_and_update(__MODULE__, {task, arg}, fn
nil ->
res = fun.(arg)
{res, res}
_value ->
# Change nothing, return current value.
:id
end)
end
end
defmodule Calc do
def fib(0), do: 0
def fib(1), do: 1
def fib(n) when n >= 0 do
Memo.calc(:fib, n, fn n -> fib(n - 1) + fib(n - 2) end)
end
end
Similar to Agent
, any changing state function given to the AgentMap
effectively blocks execution of any other function on the same key until
the request is fulfilled. So it’s important to avoid use of expensive
operations inside the agentmap. See corresponding Agent
docs section.
Finally note that use AgentMap
defines a child_spec/1
function, allowing
the defined module to be put under a supervision tree. The generated
child_spec/1
can be customized with the following options:
* `:id` - the child specification id, defauts to the current module
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
* `:restart` - when the child should be restarted, defaults to `:permanent`
* `:shutdown` - how to shut down the child
For example:
use AgentMap, restart: :transient, shutdown: 10_000
See the Supervisor
docs for more information.
Name registration
An agentmap is bound to the same name registration rules as GenServers. Read
more about it in the GenServer
documentation.
A word on distributed agents/agentmaps
See corresponding Agent
module section.
Hot code swapping
A agentmap can have its code hot swapped live by simply passing a module,
function, and arguments tuple to the update instruction. For example, imagine
you have a agentmap named :sample
and you want to convert all its inner
states from a keyword list to a map. It can be done with the following
instruction:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
The agentmap’s states will be added to the given list of arguments
([%{}]
) as the first argument.
Using Enum
module and []
-access operator
%AgentMap{}
is a special struct that contains pid of the agentmap
process
and for that Enumerable
protocol is implemented. So, Enum
should work as
expected:
iex> AgentMap.new() |> Enum.empty?()
true
iex> AgentMap.new(key: 42) |> Enum.empty?()
false
Similarly, AgentMap
follows Access
behaviour, so []
operator could be
used:
iex> AgentMap.new(a: 42, b: 24)[:a]
42
except of put_in
operator.
Installation
If available in Hex, the package can be installed
by adding agent_map
to your list of dependencies in mix.exs
:
def deps do
[
{:agent_map, "~> 0.9.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/agent_map.