Elixir: An OTP walkthrough

Gustavo Sequeira
November 3, 2017

Introduction

In the next series of posts, we'll be talking about what OTP is, and explain one of the biggest advantages of using it to help struct your code, eliminate boilerplate, hot swapping your code, supervise your applications, and many more by using the famous OTP framework. So, let's get started.

A little Disclaimer, in order to follow this series of posts we are assuming that you have erlang and elixir installed with their development environments, if not, you can go and do so right here.

What's OTP?

OTP stands for Open Telecom Platform , the platform emerged from an awesome group of four developers working for ERICSSON, a telecom company in Stockholm, Sweden. But the full name is kind of "hardcoded", because while is true that OTP was made to solve telephones exchanges and switches, it also resolves generics problems that we face now days on web development, so OTP is a general-purpose tool to manage large application now, and everyone just called it by his acronym: OTP.

You can think of OTP like a set of tools that includes this things listed below.

  • The Erlang interpreter and compiler
  • Dialyzer, a static analysis tool
  • Mnesia, a distributed database
  • Erlang Term Storage (ETS), an in-memory database
  • A debugger
  • An event tracer
  • A release-management tool

Also OTP defines systems like hierarchies trees of applications, an application is just a set of elixir processes that where modeled with some OTP behavior.

OTP Behaviour

I found particularly useful think of OTP Behaviors like design patterns for Elixir process. There are four main behaviors, that are used widely within elixir apps. But we're not limited to this four, we can actually make our own, and that's pretty cool. Here on the table we have the main behaviors and for what they are use.

Behavior For what is used to
GenServer Client-server abstraction
GenEvent Event-handling functionality
Supervisor Supervise processes functionality
Application Working with applications and defining application callbacks

First behavior: GenServer.

If we wanted it to create an abstraction of a client/server relationship, we can think that, the client/server thing is like a function, and we can call this function with a given message requesting some data, and as a result we return another message with some important data. Also a server needs a state, and the ability to change it depending on what the client wants. So basically, the recipe to make a server is some kind of messaging system (to handle incoming messages and outgoing messages) and something that takes care of the state of the server and give us the ability to change it on demand.

If we need to do this in our application, It would be very boring to implement for sure, because it has nothing to do with the business logic or the system itself, so here is where GenServer come to the rescue. GenServer Does exactly what we need and we don't have to reinvent the wheel.

Suppose we need to make some service that retrieve github information given a username, and this services will be consume for other application in out system, let see how delightfully easy is to accomplish this with GenServer behaivor and pattern-matching. Let's do this.

First of all we create the project running a mix new github on the console. After that, let's create the file libs/server.ex and put this:

defmodule Github.Server do
    use GenServer
end

This lines will bring all the GenServer's functionality on our module scope, by using some meta programming black magic. This will give us some callbacks that will help us in our quest. For example, the GenServer.start_link/3 function, this function will link the server process with the process that invoke the start_link function, and call the init/1 callback (that we need to implement).

When GenServer.start_link/3 is called, it invokes Github.Server.init/1. It waits until Github.Server.init/1 has returned, before returning. The returning values from Github.Server.init/1 are specific, they are one of the following tuples:

  • {:ok, state}
  • {:ok, state, timeout}
  • :ignore
  • {:stop, reason}

where state will be the state of the server and it could be any valid data structure, like a tree, a list, etc., in our case will be a map.

defmodule Github.Server do
    use GenServer

    ## Client API
    def start_link(opts \\ []) do
        GenServer.start_link(__MODULE__, :ok, opts)
    end

    ## Server Callbacks
    def init(:ok) do
        {:ok, %{}}
    end
end

Separate different sections of code using comments, like in the example above, is a common convention on the erlang/elixir applications and it's very useful to rapidly see what's going on. GenServer.start_link/3 accept three arguments, the first one is the modules name where the init/1 was defined, the second one is to pass it arguments to the init/1 , but in this case we don't need to do so, so the :ok atom is fine. The third is a list of options to pass it to GenServer.start_link/3 these options include defining a name for register the process and give us debugging information. But and empty list work for us, for now.

Running iex -S mix and trying our server like so:

iex(1)> {:ok, pid} = Github.Server.start_link
{:ok, #PID<0.148.0>}
iex(2)> pid
#PID<0.148.0>
iex(3)>

Nice, but our server dosen't do much, we want to send some username and get some information on github about that username. So how we achieve this? with the GenServer.call/3 function. This function make a synchronous request to the server, and expects a reply. GenServer.call/3 will call Github.Server.handle_call/3 callback, this function that we're gonna implement have specifics types of reply as the init/1 function. Here are the valid responses:

  • {:reply, reply, state}
  • {:reply, reply, state, timeout}
  • {:reply, reply, state, :hibernate}
  • {:noreply, state}
  • {:noreply, state, timeout}
  • {:noreply, state, hibernate}
  • {:stop, reason, reply, state}
  • {:stop, reason, state}

In our case we want to reply something like {:reply, user_info, state}. So let take a look on the first part of the code.

defmodule Github.Server do
    use GenServer

    @github_api_url "https://api.github.com/users"

    ## Client API
    def start_link(opts \\ []) do
        GenServer.start_link(__MODULE__, :ok, opts)
    end

    def info_of(pid, username) do
        GenServer.call(pid, {:username, username})
    end


   ## Servers Callbacks

  ...
end

@github_api_url is just a variable with the value, the URL that we want to hit. We create the function info_of, this function expect to be called with a username and the pid of the process that called it, so that the function can know how is calling, in order to retrieve the response. First we'll need two dependencies, one for making http request and the other to parse JSON files. So we're going to install: json and httpoison

After installing all the dependencies, the function handle_call should look something like this

defmodule Github.Server do

    (...)

    def handle_call({:username, username}, _from, state) do
        case get_info_of(username) do
            {:ok, user_info} ->
                new_state = update_state(state, user_info, username)
                {:reply, user_info, new_state}
            _ ->
                {:reply, :error, state}
        end
    end

    ## Helper Functions
    defp get_url_for(username) do
        "#{@github_api_url}/#{username}"
    end

    defp get_info_of (username) do
        username
        |> get_url_for
        |> HTTPoison.get
        |> parse_response
    end

    defp parse_response ({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do
        body
        |> JSON.decode!
        |> format_user_info
    end

    defp parse_response(_), do: :error

    defp format_user_info(info) do
        new_info = info
        |> Map.take(["name", "public_gists", "public_repos"])
        {:ok, new_info}
    end

    defp update_state(state, user_info, username) do
        Map.put(state, username, user_info)
    end
end

In order to make the implementation of handle_call more nice and neat, we'd split it into little helpers function. Let's discuss this lines from top to bottom.

We'll first start analyzing the handle_call, this part is using pattern matching, and a case statement, and calling get_info_of, this last function could return the user data, or and error, so, with pattern matching we're watching this two cases, if handle_call return something similar to {:ok, user_info,} we'll update the state and send back the corresponding request. And the other cases we're using the wildcard _ to match any other value from handle_call.

I personally find get_info_of so delightfully descriptive, it takes the username and gets the url for him, with get_url_for, once it gets the exact url for the user, uses the HTTPoison.get to actually make the http request, and finally call parse_response with the http response.

parse_response has two definitions, and uses pattern matching to just either take the expected response that we want (only the ones with status code 200) or raise an error.

if parse_response gets the a response with status code 200, it will parse the JSON, and call format_user_inof.

And finally, format_user_info just take the information that we want to display.

And that's it! we got the expected behavior.

Let's make one more example with handle_call just for fun, another Client endpoint called get_state that simple returns the current state of the server, could be helpful. In order to achieve this, will need to make a synchronous request to our Github.Server so we'll use handle_call this way:

defmodule Github.Server do

    # Client API
    def get_state(pid), do: GenServer.call(pid, {:get_state})

    # Server Callbacks
    defp handle_call({:get_state), do {:reply, state, state}
end

Remember that you must return some valid response, checkout this table above that summarize all the callbacks that GenServer offer and they respective valid responses:

Callbacks Valid responses
init {:ok, state}
{:ok, state, timeout}
:ignore
{:stop, reason}
handle_call {:reply, reply, state}
{:reply, reply, state, timeout}
{:reply, reply, state, :hibernate}
{:noreply, state}
{:noreply, state, timeout}
{:noreply, state, hibernate}
{:stop, reason, reply, state}
{:stop, reason, state}
handle_cast {:noreply, state}
{:noreply, state, timeout}
{:noreply, state, :hibernate}
{:stop, reason, state}
handle_info {:noreply, state}
{:noreply, state, timeout}
{:stop, reason, state}
terminate :ok
code_change {:ok, new_state}
{:error, reason}
"Elixir: An OTP walkthrough" by Gustavo Sequeira is licensed under CC BY SA. Source code examples are licensed under MIT.

Photo by Matthew Fournier.

Categorized under elixir / research & learning.

Ask about our special pricing for nonprofits.

Our priority is to help your organization fulfill its vision. In addition to accessible rates, we offer a free trial period so you can be sure our team is the solution you’re looking for.