Aug 24, 2016
Table of contents:
Over the last couple of weeks we have started to explore probably the most interesting aspect of working with Elixir (and Erlang) in that applications are built around this idea of small, isolated processes.
First we looked at Understanding Concurrency and Parallelism in Elixir and how we can run our code in isolation and make the most of a multi-core processor.
Next we looked at using Tasks as an abstraction of the basic spawn function. Tasks make it really easy to move sequential code off into a separate process so you can make the most of concurrency.
Over the last two weeks we’ve looked at storing state in Elixir processes, and then using Agents as an abstraction of state so we don’t have to write the boilerplate code ourselves. Storing state in a functional programming language requires us to continuously pass the state to a recursive function. Fortunately the Erlang Virtual Machine is very good at dealing with this type of work.
The next step of our exploration is to investigate GenServer. Whilst Agents are specifically used for storing state, GenServer is used as a general purpose abstraction for building generic servers in Elixir.
In today’s tutorial we will be exploring the how and why of using GenServer.
One of the core tenants of using Elixir (and Erlang) is that applications are built around isolated processes. This is such a common thing that it makes sense to have a standard way of writing this type of code.
Erlang has a module called :gen_server
that defines a number of functions that should be implemented by a module that is a “generic server”. If you are familiar with other programming languages, :gen_server
is a bit like an interface. In Erlang, a module that defines the functions that should be implemented by another module is called a behaviour.
In Elixir we have a module called GenServer
. This Elixir module is a totally separate thing to the :gen_server
module of Erlang. GenServer
is a module that you add to your implementation module through the use
macro:
defmodule ShoppingList do
use GenServer
end
We haven’t looked at using macros in Elixir as that is a whole other topic to explore. However, in simple terms, when you use GenServer
, Elixir is basically providing default implementations for the :gen_server
behaviour module.
This means that any module that adds the GenServer
module will automatically be a compliant :gen_server
module. This allows you to only implement the functions that you require in your module, whilst leaving the rest of the functions to fall back to the defaults provided by GenServer
.
So hopefully you now have a good idea of what GenServer
is and how you would add it to your module. The next question is, “how do you use GenServer?”.
To use GenServer you basically just implement the functions that you require of your generic server. You don’t need to implement all of the functions of :gen_server
because the GenServer
module from Elixir will provide the default implementations.
The functions of a GenServer are basically split between the client and the server. As we saw in Working with State and Elixir Processes, you call the public API of the process from the client, and then you handle those requests on the server. This can be a bit confusing as the methods are part of the same module, but to make things clearer in this tutorial I will mark the split between the client and the server.
For the rest of this tutorial we will look at implementing the functions of GenServer.
The first function you will need to implement is for starting the server:
def start_link do
GenServer.start_link(__MODULE__, :ok, [])
end
As you can see, in this function we simply need to call the start_link
function on the GenServer
module and pass it three arguments.
The first argument is providing the location of where the server callbacks are implemented. In this example I’m going to implement them as part of the same module and so I can use __MODULE__
instead of writing the name of the module. You could of course split the server callback functions into another module if you wanted to.
The next argument is a the initialisation arguments for the module. I’m not going to require any initialisation arguments in this example so I can simply provide the :ok
atom.
And finally, the third argument is a list of options that can be used to set certain values. Again we don’t need to worry about these options right now so I will just provide an empty list as the third argument.
Next we need to provide the public API for the module. These are the functions that will be invoked by other modules of your application:
def read(pid) do
GenServer.call(pid, {:read})
end
def add(pid, item) do
GenServer.cast(pid, {:add, item})
end
In the example above I’ve added two functions, one for reading the shopping list and one for adding a new item to the shopping list.
The read/1
function accepts the pid of the process as the only argument. We can use the call/2
function of the GenServer.module
to send the request to the process. When using the call/2
function, the process will block until it has received a response from the server.
The add/2
function accepts the pid of the process and the item to add to the shopping list. This time I’ve used the cast/2
function. This function will return immediately as it does not wait for a response from the server.
On one hand, the fact that the response is immediate is a good thing, but on the other hand you don’t know if the item was successfully added to the list. Its up to your discretion to chose the appropriate function for your use case. Generally speaking, if you want to make sure the request was successful, use call/2
.
Now that we have a way to start the process and a couple of functions to interact with as a public API, it’s now time to turn our attention to implementing the server side functions.
The first function I will implement will be for initialising the server:
def init(:ok) do
{:ok, []}
end
This function is automatically called first when the process is started via the start_link
function. As you can see, the only argument of this function is the argument we passed as the second argument to the start_link
function from earlier (in this case :ok
).
In this function you can do whatever you need to do to initialise the server. The return value of this function should be a tuple where the first item is the atom :ok
and the second argument is the initial state of the server (in this case an empty list).
Next we need to handle the incoming requests from the public API. First we will handle the :read
request:
def handle_call({:read}, _from, list) do
{:reply, list, list}
end
As you can see, to handle call
requests we implement the handle_call/3
function and then we use pattern matching to handle the specific request by matching against the arguments of the request.
The second argument of this function is the pid of the process that sent the request. For the most part you can just ignore this value. To ignore the value you can use an underscore and optionally give it a name. If you don’t use an underscore the Erlang compiler will complain that there is an unused variable.
And finally the third argument is the current state.
The return value of a handle_call/3
function should be a tuple where the first argument is the atom :reply
, the second argument is the value to be returned to the client, and the third argument should be the state. In this case I want to return the full list so the second and third arguments are just the same.
Next we can handle the cast
request:
def handle_cast({:add, item}, list) do
{:noreply, list ++ [item]}
end
Once again we use the generic handle_cast/2
function and then allow pattern matching to work it’s magic to find the correct implementation for the given arguments.
Handling cast
requests is a bit simpler than handling call
requests. The first difference is that we don’t have the from
pid from the client in the arguments to this function because we never need to send a response.
In the body of the function you would handle the request. In this case I’m simply appending the item to the end of the list.
The return value of this function should be a tuple where the first item is the atom :noreply
and the second item is the state.
Here is our new ShoppingList
module that implements GenServer:
defmodule ShoppingList do
use GenServer
# Client API
def start_link do
GenServer.start_link(__MODULE__, :ok, [])
end
def read(pid) do
GenServer.call(pid, {:read})
end
def add(pid, item) do
GenServer.cast(pid, {:add, item})
end
# Server Callbacks
def init(:ok) do
{:ok, []}
end
def handle_call({:read}, from, list) do
{:reply, list, list}
end
def handle_cast({:add, item}, list) do
{:noreply, list ++ [item]}
end
end
To use this module, fire up iex
and pass the name of the file at the command prompt:
iex shopping_list.ex
Next we can walk through starting the server, adding a couple of items, and then reading those items back:
# Start the server
{:ok, pid} = ShoppingList.start_link()
# Adding some items
ShoppingList.add(pid, "milk")
ShoppingList.add(pid, "bread")
ShoppingList.add(pid, "cheese")
# Read the items back
ShoppingList.read(pid)
This was a basic introduction to understanding GenServer, what it is, why it’s important, and how to use it.
GenServer
is a specification for writing generic server processes in Elixir. It is simply a module of Elixir that provides default implementations for the :gen_server
Erlang behaviour.
You could implement :gen_server
yourself, but it is the product of a lot of hardwork from very clever people. By using the :gen_server
behaviour it also makes it a lot easier to read and understand other Elixir and Erlang code.
There are still a couple of things we haven’t covered in respect to GenServer, including implementing the other functions of the :gen_server
module.
But understanding what, why, and how is the most important stuff, everything else is better handled with an actual good use case for the functionality. As we explore more complex uses of GenServer we will no doubt cover all of the functionality of the module.
In the simple example we have looked at today we could of just used an Agent as we just need to store state. Agents are just simple versions of GenServer, but by using GenServer we have a lot more power.
We will explore that power in the coming weeks.