# AgentMap
The `AgentMap` can be seen as a stateful `Map` that parallelize operations
made on different keys.
For instance, this call:
```elixir
iex> fun =
...> fn v ->
...> :timer.sleep(10)
...> {:_get, v + 1}
...> end
...>
iex> map = Map.new(a: 1, b: 1)
iex> {:_get, map} = Map.get_and_update(map, :a, fun)
iex> {:_get, map} = Map.get_and_update(map, :b, fun)
iex> Map.get(map, :a)
2
iex> Map.get(map, :b)
2
```
will be executed in `20` ms, while this:
```elixir
iex> fun =
...> fn v ->
...> :timer.sleep(10)
...> {:_get, v + 1}
...> end
...>
iex> am = AgentMap.new(a: 1, b: 1)
iex> AgentMap.get_and_update(am, :a, fun)
:_get
iex> AgentMap.get_and_update(am, :b, fun)
iex> AgentMap.get(am, :a)
2
iex> AgentMap.get(am, :b)
2
```
in around of `10` ms, because of parallelization.
Underneath it's a `GenServer` that holds a `Map`. When a state changing call is
first made for a key (`update/4`, `update!/4`, `get_and_update/4`, …), a special
temporary process called "worker" is spawned. All subsequent calls for that key
will be forwarded to the message queue of this worker. This process respects the
order of incoming new calls, executing them in a sequence, except for `get/4`
calls, which are processed as a parallel `Task`s. For each key, the degree of
parallelization can be tweaked using `max_processes/3` function. The worker will
die after about `10` ms of inactivity.
The `AgentMap` supports multi-key calls — operations made on a group of keys.
See `AgentMap.Multi`.
Basically, `AgentMap` can be used as a cache, memoization, computational
framework and, sometimes, as a `GenServer` replacement.
See documentation for [AgentMap](https://hexdocs.pm/agent_map).
## Examples
Create and use it as an ordinary `Map`:
```elixir
iex> am = AgentMap.new(a: 42, b: 24)
iex> AgentMap.get(am, :a)
42
iex> AgentMap.keys(am)
[:a, :b]
iex> am
...> |> AgentMap.update(:a, & &1 + 1)
...> |> AgentMap.update(:b, & &1 - 1)
...> |> AgentMap.take([:a, :b])
%{a: 43, b: 23}
```
The special struct `%AgentMap{}` can be created via the `new/1` function. This
allows to use the `Enumerable` protocol.
Also, `AgentMap` can be started in an `Agent` manner:
```elixir
iex> {:ok, pid} = AgentMap.start_link()
iex> pid
...> |> AgentMap.put(:a, 1)
...> |> AgentMap.get(:a)
1
iex> pid
...> |> AgentMap.new()
...> |> Enum.empty?()
false
```
More complicated example involves memoization:
```elixir
defmodule Calc do
def fib(0), do: 0
def fib(1), do: 1
def fib(n) when n >= 0 do
unless GenServer.whereis(__MODULE__) do
AgentMap.start_link([], name: __MODULE__)
fib(n)
else
AgentMap.get_and_update(__MODULE__, n, fn
nil ->
# This calculation will be made in a separate
# worker process.
res = fib(n - 1) + fib(n - 2)
# Return `res` and set it as a new value.
{res, res}
_value ->
# Change nothing, return current value.
:id
end)
end
end
end
```
Take a look at the
[test/memo.ex](https://github.com/zergera/agent_map/blob/master/test/memo.ex).
The `AgentMap` provides possibility to make multi-key calls (operations on
multiple keys). Let's see an accounting demo:
```elixir
defmodule Account do
def start_link() do
AgentMap.start_link([], name: __MODULE__)
end
def stop() do
AgentMap.stop(__MODULE__)
end
@doc """
Returns `{:ok, balance}` or `:error` in case there is no
such account.
"""
def balance(account) do
AgentMap.fetch(__MODULE__, account)
end
@doc """
Withdraws money. Returns `{:ok, new_amount}` or `:error`.
"""
def withdraw(account, amount) do
AgentMap.get_and_update(__MODULE__, account, fn
nil -> # no such account
{:error} # (!) refrain from returning `{:error, nil}`
# as it would create key with `nil` value
balance when balance > amount ->
balance = balance - amount
{{:ok, balance}, balance}
_balance ->
# Returns `:error`, while not changing value.
{:error}
end)
end
@doc """
Deposits money. Returns `{:ok, new_amount}` or `:error`.
"""
def deposit(account, amount) do
AgentMap.get_and_update(__MODULE__, account, fn
nil ->
{:error}
balance ->
balance = balance + amount
{{:ok, balance}, balance}
end)
end
@doc """
Trasfers money. Returns `:ok` or `:error`.
"""
def transfer(from, to, amount) do
# Multi call.
AgentMap.Multi.get_and_update(__MODULE__, [from, to], fn
[nil, _] -> {:error}
[_, nil] -> {:error}
[b1, b2] when b1 >= amount ->
{:ok, [b1 - amount, b2 + amount]}
_ -> {:error}
end)
end
@doc """
Closes account. Returns `:ok` or `:error`.
"""
def close(account) do
AgentMap.pop(__MODULE__, account) && :ok || :error
end
@doc """
Opens account. Returns `:ok` or `:error`.
"""
def open(account) do
AgentMap.get_and_update(__MODULE__, account, fn
nil ->
# Sets balance to 0, while returning :ok.
{:ok, 0}
_balance ->
# Returns :error, while not changing balance.
{:error}
end)
end
end
```
## Installation
`AgentMap` requires Elixir `v1.8` Add `:agent_map`to your list of dependencies
in `mix.exs`:
```elixir
def deps do
[{:agent_map, "~> 1.0"}]
end
```
## License
[MIT](https://github.com/zergera/agent_map/blob/dev/LICENSE).