# 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](https://hex.pm/docs/publish), the package can be installed
by adding `agent_map` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:agent_map, "~> 0.9.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/agent_map](https://hexdocs.pm/agent_map).