Subscribe to access all episodes. View plans →
Published April 16, 2017
Elixir 1.4.2
Source code on GitHub
Elixir comes with a set of libraries known as OTP.
One of those libraries is the GenServer
module, which provides a common interface to help handle concurrency.
In this episode we’ll get started with the basics of GenServer
by using it to help us manage a shopping list.
Let’s get started.
Here we have an empty module called ‘ShoppingList’.
First we’ll add the use macro to bring in the GenServer
module.
Now, let’s create a function named start_link, which we’ll use to start our shopping list process.
And inside it we’ll call the GenServer
function of the same name, start_link.
We’ll use __MODULE__
to return our ShoppingList
module, and then we’ll pass in any initial state we want to start with. We’ll use an empty list.
Once we call GenServer.start_link/3
, the ‘init’ callback is triggered.
Let’s define it.
It takes one parameter, which is the initial state of an empty list included as the second argument in GenServer.start_link/3
.
We’ll call it ‘list’ since it represents our shopping list.
‘init’, along with most other GenServer
functions, has a specific format it needs to return.
We’ll keep ours simple and return a 2 element tuple {:ok, 'list'}
to get our ShoppingList
started.
For a full list of return values the different GenServer
functions expect, refer to the GenServer docs.
shopping_list.ex
defmodule ShoppingList do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, [])
end
def init(list) do
{:ok, list}
end
end
With that we can let’s fire up an IEx session and see what starting our GenServer
looks like.
$ iex shopping_list.ex
The GenServer.start_link/3
and by extension our ShoppingList.start_link
function will return a tuple with :ok and our process identifier or PID.
We’ll pattern match to grab the PID.
> {:ok, pid} = ShoppingList.start_link()
{:ok, #PID<0.111.0>}
Let’s check to make sure everything started correctly and that our process is still alive.
> Process.alive?(pid)
true
Great, our shopping list is up and running. Now let’s add a way to add and remove items to it.
Let’s go back to our module.
One important concept regarding GenServer
is that they’re organized into two parts: the client and the server, each running in their own process.
Now you can split the client and server into separate modules, but for the purposes of our demo, we’ll stay simple and keep everything in the same module.
But we will organize our functions by whether they’re part of the client or the server.
Let’s start with our ‘start_link’ function, which is part of our ‘client’ api.
And our ‘init’ callback is part of our server callbacks.
shopping_list.ex
defmodule ShoppingList do
use GenServer
#Client
def start_link do
GenServer.start_link(__MODULE__, [])
end
#Server
def init(state) do
{:ok, state}
end
end
Great, now let’s create a function for our client API named ‘add’, which will add items to our shopping list. It will take a PID and the item we want to add to it.
GenServer
gives us two options on how we want to update our shopping list: synchronously, where we care about a response. And asynchronously, where we don’t care about a response.
Since we’re just adding items to our shopping list, we don’t necessarily care about a response so we’ll use GenServer.cast/2
to send an asynchronous request to the server.
GenServer.cast/2
will take the PID we passed in and the item we want to add to our shopping list.
And when GenServer.cast/2
is called, the ‘handle_cast’ server callback is invoked. Let’s add that now.
It will accept two parameters: our item, and our current shopping list.
Then we can update our our shopping list.
‘handle_cast’ expects a two element tuple as its return, with ‘:noreply’ as the first element and the updated shopping list as the second element.
Now that we can add items to our shopping list, let’s add a way to view it.
We’ll add another function to our client API named ‘view’, which will take a PID.
Since we care about a response here, we’ll use the synchronous GenServer.call/3
And we’ll pass in the PID we received and the atom ‘view’.
This will invoke the ‘handle_call’ server callback, so let’s implement that too.
The “view” atom we used will be passed in as the first argument for our ‘handle_call’ function, so let’s pattern match on it.
The second argument passed in is a two element tuple that includes the caller’s PID. We don’t need it for this demo, so let’s ignore it.
And finally the third argument is our existing state - in this case our current shopping list.
Since we just want to return our shopping list, according to the docs we’ll need to return a three element tuple, with ‘:reply’ as the first element.
The second element is what we want to return, in this case our shopping list.
And finally we need to return our shopping list as the state we want to continue with as the third element.
shopping_list.ex
defmodule ShoppingList do
use GenServer
#Client
def start_link do
GenServer.start_link(__MODULE__, [])
end
def add(pid, item) do
GenServer.cast(pid, item)
end
def view(pid) do
GenServer.call(pid, :view)
end
#Server
def init(list) do
{:ok, list}
end
def handle_cast(item, list) do
updated_list = [item|list]
{:noreply, updated_list}
end
def handle_call(:view, from, list) do
{:reply, list, list}
end
end
With that let’s test it out.
Going back to our terminal, we’ll reload our module.
Now we need to add a few items to our shopping list it. Let’s add eggs, milk, and cheese.
Then let’s see if we can view our shopping list.
And great it worked.
> {:ok, pid} = ShoppingList.start_link()
{:ok, #PID<0.86.0>}
> ShoppingList.add(pid, "eggs")
:ok
> ShoppingList.add(pid, "milk")
:ok
> ShoppingList.add(pid, "cheese")
:ok
> ShoppingList.view(pid)
["cheese", "milk", "eggs"]
Let’s go back to our module and add a way for us to remove items from our shopping list.
Let’s name our new function ‘remove’ which will take our shopping list PID and the item we want to remove from the list.
We’ll then call GenServer.cast/2
since we don’t care about the response.
Again we’ll pass in the PID, and for our second argument we’ll use a two element tuple.
The first element will be an atom ‘remove’, which we can pattern match with in our callback.
Then we’ll use the item we want to remove as the second element.
Now we can define our corresponding ‘handle_cast’ callback.
In the first parameter we’ll pattern match against the first element, the atom “remove”, that we used in our two element tuple above.
Then our existing list will be passed in the second parameter.
One note - this ‘handle_cast’ function will never match unless it comes first.
Now we need to remove the item from our shopping list. We’ll use Enum.reject/2
And pass in our existing list.
Then we’ll write a function to return true if any item in the list matches what was passed in.
And then we can return another two element tuple with ‘:noreply’ and our updated list.
shopping_list.ex
defmodule ShoppingList do
use GenServer
#Client
def start_link do
GenServer.start_link(__MODULE__, [])
end
def add(pid, item) do
GenServer.cast(pid, item)
end
def view(pid) do
GenServer.call(pid, :view)
end
def remove(pid, item) do
GenServer.cast(pid, {:remove, item})
end
# Server
def init(list) do
{:ok, list}
end
def handle_cast({:remove, item}, list) do
updated_list = Enum.reject(list, fn(i) -> i == item end)
{:noreply, updated_list}
end
def handle_cast(item, list) do
updated_list = [item|list]
{:noreply, updated_list}
end
def handle_call(:view, _from, list) do
{:reply, list, list}
end
end
Let’s go back to the terminal and reload our module.
Then let’s try removing an item from the list.
> r(ShoppingList)
warning: redefining module ShoppingList (current version defined in memory)
shopping_list.ex:1
{:reloaded, ShoppingList, [ShoppingList]}
> ShoppingList.view(pid)
["cheese", "milk", "eggs"]
> ShoppingList.remove(pid, "cheese")
:ok
> ShoppingList.view(pid)
["milk", "eggs"]
And great it was removed from our shopping list.
Now we just need a way to stop our ‘ShoppingList’ process.
Let’s go back to our module.
We’ll add a function named ‘stop’ to the client API that takes the PID we want to stop.
Then we’ll call GenServer.stop/3
with the PID that was passed in.
The shutdown reason of “normal”. And a timeout of “infinity”.
Since ‘normal’ and ‘infinity’ are default values, we don’t need to include them here, but I’ll leave them for clarity.
Now let’s implement the corresponding callback ‘terminate’.
It will take a ‘reason’ which we specified as ‘normal’. And since we don’t care about it for this demo, let’s ignore it.
And then our current shopping list.
Let’s also print out a message to help us see our shopping list shutting down.
And then we need to return a single atom :ok
shopping_list.ex
defmodule ShoppingList do
use GenServer
#Client
def start_link() do
GenServer.start_link(__MODULE__, [])
end
def add(pid, item) do
GenServer.cast(pid, item)
end
def view(pid) do
GenServer.call(pid, :view)
end
def remove(pid, item) do
GenServer.cast(pid, {:remove, item})
end
def stop(pid) do
GenServer.stop(pid, :normal, :infinity)
end
#Server
def terminate(_reason, list) do
IO.puts("We are all done shopping.")
IO.inspect(list)
:ok
end
def handle_cast({:remove, item}, list) do
updated_list = Enum.reject(list, fn(i) -> i == item end)
{:noreply, updated_list}
end
def handle_cast(item, list) do
updated_list = [item|list]
{:noreply, updated_list}
end
def handle_call(:view, _from, list) do
{:reply, list, list}
end
def init(list) do
{:ok, list}
end
end
Going back to our terminal one last time. Let’s reload our ShoppingList
module.
Then let’s double check the items in our shopping list
> ShoppingList.view(pid)
["milk", "eggs"]
Now let’s try stopping our Shopping List process.
>ShoppingList.stop(pid)
We are all done shopping.
["milk", "eggs"]
:ok
>Process.alive?(pid)
false
And if we can check and see that the process is was stopped.
alanbar
7 years agoGreat simple intro, thank you. Would it be possible to show how to leverage this business logic into a Phoenix framework for general access, as outlined in the upcoming book “Functional Web Development with Elixir, OTP, and Phoenix” and the video “Phoenix is not your application”? I have attempted to follow the video but cannot get it to work but I am new to this.
Alekx
7 years agoHey alanbar, I’ll be working on some content like this in the next few months.
alanbar
7 years agoThat will be really useful, there seem to be no examples of how to do this at present.