Looking for clarity around using agent

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 pidhas 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.

4 Likes