Subscribe to access all episodes. View plans →
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 pid
s 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.
Neil Lyons
5 years agoWhat is an Agent used for? Is it sort of like global variables in Python?
Wouldn’t restarting the server (eg on a deploy) mean you would lose any state kept by the Agent?
Alekx
5 years agoHey Neil,
Agents are simply wrappers around state. This is useful in Elixir, which is an immutable language where by default nothing is shared.
Restarting the server would cause you to lose any data kept by the Agent. However, in Elixir there are Hot Upgrades that allow you to deploy new code without restarting and losing that data. Gigalixir is Heroku-like platform built specifically for Elixir that makes this much easier.