Subscribe to access all episodes. View plans →

#32: Intro to Agents

Published January 8, 2018

Elixir 1.4.5

View source on GitHub


In Elixir Agents are specialized GenServers that are all about state.

If you see an Agent being used you can be pretty confident that it’s being used to manage some state.

Let’s open iex and see how we can use an Agent to manage some state.

Now there are two ways to start an Agent.

Agent.start_link will start an agent linking it to the current process, helpful if you’re using an agent as part of a supervision tree.

And Agent.start which starts an agent without linking it.

Let’s use Agent.start_link.

And we’ll give it an anonymous function. The return value of that function will be used as our initial state. Let’s have ours return an empty list.

If successful this will return an ok tuple where the second element is the pid, or process identifier, of our Agent.

And we can see that our process is alive and running.

> {:ok, pid} = Agent.start_link(fn() -> [] end)
{:ok, #PID<0.92.0>}
> Process.alive?(pid)
true

Now that we have our agent, how do we update it?

We can use Agent.update to update our list.

It will take our pid as the first argument.

And then a function, where we can pass in the state, updating it how we want. The return of this function will be our new state.

Let’s take our state to add 1 to our empty list.

> Agent.update(pid, fn(state) -> [1|state] end)
:ok

And it returns :ok to let us know everything was updated.

Now lets see what our state looks like.

We’ll use Agent.get, which will again take our pid as the first argument. And a function as the second argument. We’ll again pass our state into our function. And the return of this function is what will be returned.

We’ll just return our state.

> Agent.get(pid, fn(state) -> state end)
[1]

Perfect it returned a list with 1 which is just what we expected.

Let’s close iex

And now that we know how to update and get the state of an Agent, let’s expand on this by using one in a module.

Let’s create a module that we can use to manage some state about different movies.

First we’ll create a file named movie_data.exs.

Then we can define our module, MovieData.

Now let’s create a function on our module to start our agent.

Let’s call it start_link and it will just wrap the Agent.start_link function. And we’ll return an empty map for our initial state.

Now let’s outline what we want our module to do.

We want to be able to add movies. We’ll also want to be able to reset our movies.

Finally let’s also track the number of times a movie has been watched. Since our state is a map, we’ll use the name of the movie for the key and its value will be the number of times it’s been watched.

Let’s start with adding a movie.

We’ll create a new function named add. It will take our pid and then our movie.

Inside the function we’ll call Agent.update passing it the pid of our Agent. Then we’ll create an anonymous function that will take our state.

We’ll then call Map.put to update our existing state with our new movie. And we’ll give it an initial watch count of 1.

Now let’s create another function named reset that we can use to reset our state. It will take a pid.

Inside it we’ll call Agent.update and we’ll ignore our state since we won’t need it. And inside our function we’ll just return an empty map.

Now let’s create a function view the number of watches a movie has.

We’ll create a new function named watch_count. It will take a pid and a movie that we want to watch.

Now to return a value we’ll use Agent.get with our pid and we’ll create another anonymous function, passing in our state.

Then we’ll call Map.get - this will return the value of our movie key, which will work perfectly since it’s what we’re using for the number of watches our movie has.

Now we just need a way to increment the number of watches a movie has.

Let’s do this with another function watch that will take a pid and the name of the movie whose watch count we want to increment.

Since we’ll be updating our state we’ll call Agent.update passing in our pid and in our anonymous function let’s first get the current count with Map.get.

Then let’s use Map.replace to update our count. We’ll pass in our state, the movie we want to update and then we’ll set increment the current count by one. And because Map.replace will return our whole map with the updated value, we can just return this for our new state.

movie_data.exs

defmodule MovieData do

  def start_link do
    Agent.start_link(fn -> %{} end)
  end

  def add(pid, movie) do
    Agent.update(pid, fn(state) ->
      Map.put(state, movie, 1)
    end)
  end

  def reset(pid) do
    Agent.update(pid, fn(_state) -> %{} end)
  end

  def watch_count(pid, movie) do
    Agent.get(pid, fn(state) ->
      Map.get(state, movie)
    end)
  end

  def watch(pid, movie) do
    Agent.update(pid, fn(state) ->
      count = Map.get(state, movie)
      Map.replace(state, movie, count + 1)
    end)
  end
end

Now let’s test it out.

We’ll go to the command line and run iex with our filename:

$ iex movie_data.exs

Alright, now let’s create our new Agent.

> {:ok, pid} = MovieData.start_link
{:ok, #PID<0.90.0>}

Now let’s test to see if a count exists for a movie.

> MovieData.watch_count(pid, :casablanca)
nil

And great it returned nil.

Let’s add a movie and try again.

And great the count was returned.

Now let’s ‘watch’ it a few times.

And running watch_count returns an updated count.

Then let’s reset our movies.

And if we run our watch_count again it return nil. Great, Our module is working as expected.

> MovieData.add(pid, :casablanca)
:ok
> MovieData.watch_count(pid, :casablanca)
1
> MovieData.watch(pid, :casablanca)
:ok
> MovieData.watch(pid, :casablanca)
:ok
> MovieData.watch_count(pid, :casablanca)
3
> MovieData.reset(pid)
:ok
> MovieData.watch_count(pid, :casablanca)
nil

If you’re new to Elixir you may be wondering why we have to pass around the pid or process identifier around. This is because our Agent IS a process. So we need to keep track of this and any other Agent pids that we want to access state from.

One alternative to this approach is to name our process. If we look at the [docs] [https://hexdocs.pm/elixir/Agent.html#start\_link/2] we see there’s an name option we can use.

Let’s update our module to see what that would look like.

We’ll go to our start_link function and add the name as the current module.

Then we can go through our functions and remove pid as a parameter.

And in the places where we were calling pid we can change it to use the current module.

movie_data.exs

defmodule MovieData do

  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end

  def add(movie) do
    Agent.update(__MODULE__, fn(state) ->
      Map.put(state, movie, 1)
    end)
  end

  def reset do
    Agent.update(__MODULE__, fn(_state) -> %{} end)
  end

  def watch_count(movie) do
    Agent.get(__MODULE__, fn(state) ->
      Map.get(state, movie)
    end)
  end

  def watch(movie) do
    Agent.update(__MODULE__, fn(state) ->
      count = Map.get(state, movie)
      Map.replace(state, movie, count + 1)
    end)
  end
end

Then let’s start iex back up.

$ iex movie_data.exs

And we’ll start our agent ignoring the return values since we don’t need them.

And we’ll add movie to our Agent.

Getting the count still returns 1.

Then let’s increment the count.

And it updated the count.

Finally let’s reset our movies - and great everything is still working as expected.

> MovieData.start_link
{:ok, #PID<0.90.0>}
> MovieData.add(:casablanca)
:ok
> MovieData.watch_count(:casablanca)
1
> MovieData.watch(:casablanca)
:ok
> MovieData.watch(:casablanca)
:ok
> MovieData.watch_count(:casablanca)
3
> MovieData.reset
:ok
> MovieData.watch_count(:casablanca)
nil
> MovieData.start_link
{:error, {:already_started, #PID<0.90.0>}}

And if we try and create another Agent, we get an error with the message “already started”, because we’ve already got a process with this name running.

© 2024 HEXMONSTER LLC