Hubert’s reply is correct
I’d like to explore some aspects in more depth and detail.
First of all many of us are still scratching our head in terms of finding valid use cases for Agents as evidenced by this topic:
Discussion about uses for Agent Processes
Hubert’s use case of changing the configuration state of a running system through the shell is a viable one - though I would imagine that sending a function to update the state is inherently more risky than simply replacing the old state with a new, known to be consistent, state.
Then there is the choice of your example - it is inherently sequential. I recommend that you watch:
Erlang Master Class 2: Video 1 - Turning sequential code into concurrent code
Your example forces a particular ordering on the sequence of operations which doesn’t take advantage of the capabilities of the Agent - there are no independent, concurrent parts in your computation.
Your use of an Agent can be roughly expressed as a GenServer
like this
defmodule M do
use GenServer
# instead of fn -> [1, 2, 3] end
def create_object() do
{:ok, pid} = GenServer.start_link(__MODULE__, [1,2,3])
pid
end
def update_object(pid, new_data) do
GenServer.call(pid,{:update, new_data})
pid
end
def get_object(pid) do
IO.inspect GenServer.call(pid,:get)
pid
end
# GenServer callbacks
def init(state) do
# TODO initialization logic
{:ok, state}
end
# instead of `fn (state) -> state ++ new_data end`
def handle_call({:update, new_data}, _from, state) do
{:reply, :ok, state ++ new_data}
end
# instead of `&(&1)`
def handle_call(:get, _from, state) do
{:reply, state, state}
end
end
M.create_object() |> M.update_object([4,5]) |> M.get_object()
Agents are often used to introduce the concept of processes because the code looks initially much less arcane than GenServer
code. In your example the client code provides the function that is updating the Agent’s state. In my GenServer
based code I fixed the “meaning” of “update” in a module function to appending new_data
to the GenServer
state
.
The point I’m trying to make is that Agent
can be viewed as a GenServer
turned-inside-out. With a GenServer
the functionality to change process state is fixed within the callback module - with an Agent
the computations (functionality) to change process state are provided from outside of the process.
The whole point for an Agent
:
Agent.update(pid, fn (state) -> state ++ new_data end)
or a GenServer
GenServer.call(pid,{:update, new_data})
is that numerous (tens, hundreds, thousands, … of) other processes can concurrently append data to the list managed by process pid
without sharing state.
In a typical conventional multi-threaded program that list would often be shared so that each thread could append its own data to the shared list - so the list would have to be explicitly protected by locks, mutexes, etc.
Now Agent.update
and GenServer.call
are synchronous calls - so they will block until the target process receives the message and sends a reply. For asynchronous processing there is Agent.cast
and GenServer.cast
def update_object(pid, new_data) do
GenServer.cast(pid,{:update, new_data})
pid
end
...
# instead of `fn (state) -> state ++ new_data end`
def handle_cast({:update, new_data}, state) do
{:noreply, state ++ new_data}
end
Now when update_object/2
returns there is no guarantee that the list in pid
has been updated yet. But the code still works - because get_object/1
is still synchronous and because here messages are processed in the order of arrival, i.e. {:update,new_data}
before :get
, the list will be updated before the state is returned.