An intro to OTP in Elixir

In this installment in our series of introductory Elixir blog posts we’ll convert the FizzBuzz server from last time into an OTP app.

What is OTP?

According to the documentation, OTP — the Open Telecom Platform — is “a complete development environment for concurrent programming”, containing an Erlang compiler and interpreter, a database server (Mnesia), an analysis tool (Dyalizer), as well as a lot of libraries. This latter part is what people generally refer to when talking about OTP.

Behaviours

One of the central design principles of Erlang/OTP are application patterns, or “behaviours” in OTP lingo. They define generic implementations for common tasks, for example a generic server (gen_server) module. The application developer adds implementation specific code into a callback module which exports a set of specific functions. Unlike Erlang, Elixir provides default implementations for these functions, so you only have to override the ones you need.

A FizzBuzz server using GenServer

Enough theory, let’s write some code!

We use mix to create a new project. Since we want the module name to be in CamelCase we have to provide the — module option, otherwise it would default to Fizzbuzz.
$ mix new fizzbuzz --module FizzBuzz

This command creates a whole application template, but for the purposes of this blog post we’ll only work on lib/fizzbuzz.ex.

Let’s start by letting our application know that we are implementing the GenServer OTP behaviour:
use GenServer
require Logger

We also required the Logger module, since we are using it for debugging purposes. Next we define the API for our clients.
def start_link do
   GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

def get(n) do
  GenServer.call(__MODULE__, {:print, n})
end

def print(n) do
  GenServer.cast(__MODULE__, {:print, n})
end

FizzBuzz.start_link/0 is just a wrapper around GenServer.start_link/3which starts the server as a linked process, often as part of a supervision tree. Note that we start our server with the name option for which we use the module name (__MODULE__). This way other functions don’t have to refer to our server by PID.

FizzBuzz.get/1 and FizzBuzz.print/1 are our server’s main interface. They receive a number as an argument and return or output the corresponding FizzBuzz value. GenServer supports two request types: calls and casts. The former is synchronous and supposed to send something back to the client (our get function), whereas the latter is asynchronous and doesn’t necessarily return anything to the client (our print function).

It’s common to wrap the GenServer interface in our own client APIs, so the above pattern is worth remembering.

With that out of the way, we now go on to implement the necessary GenServer callbacks.
def init(:ok) do
  Logger.debug "FizzBuzz server started"
  {:ok, %{}}
end

def handle_call({:print, n}, _from, state) do
  {:ok, fb, state} = fetch_or_calculate(n, state)
  {:reply, fb, state}
end

def handle_cast({:print, n}, state) do
  {:ok, fb, state} = fetch_or_calculate(n, state)
  IO.puts fb
  {:noreply, state}
end

init/1 gets called by GenSever.start_link/3 and returns a tuple of the form {:ok, state}. In our specific case the state is a simple Elixir map (%{}).

handle_call/2 and handle_cast/2 are the core of our application. We use pattern matching on the first argument to specify that we handle messages of the form {:print, n}. The private function fetch_or_calculate/2 retrieves or calculates the value, and we then return the appropriate response. In the case of a call it will have the form {:reply, response, state}, whereas for a cast it will be {:noreply, state}. These are GenServer conventions, the documentation lists all possible answer types for every request type.

This is what the private helper looks like:
defp fetch_or_calculate(n, state) do
  if Dict.has_key?(state, n) do
    Logger.debug "Fetching #{n}"
    {:ok, fb} = Dict.fetch(state, n)
  else
    Logger.debug "Calculating #{n}"
    fb = fizzbuzz(n)
    state = Dict.put(state, n, fb)
  end
  {:ok, fb , state}
end

fetch_or_calculate/2 checks if the FizzBuzz value for n has been calculated before. If it has, we fetch it from the dictionary. If the value hasn’t been computed before, we do so and update the state by adding the newly computed value to it (Dict.put(state, n, fb)). Finally we return a tuple of the form {:ok, value, state} which we pattern match against in the handler functions.

That’s it, our FizzBuzz server is now ready for action! Here’s the complete code:
defmodule FizzBuzz do
  use GenServer
  require Logger

  ## Client API

  def start_link do
     GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def get(n) do
    GenServer.call(__MODULE__, {:print, n})
  end

  def print(n) do
    GenServer.cast(__MODULE__, {:print, n})
  end

  ## Server Callbacks

  def init(:ok) do
    Logger.debug "FizzBuzz server started"
    {:ok, %{}}
  end

  def handle_call({:print, n}, _from, state) do
    {:ok, fb, state} = fetch_or_calculate(n, state)
    {:reply, fb, state}
  end

  def handle_cast({:print, n}, state) do
    {:ok, fb, state} = fetch_or_calculate(n, state)
    IO.puts fb
    {:noreply, state}
  end

  defp fetch_or_calculate(n, state) do
    if Dict.has_key?(state, n) do
      Logger.debug "Fetching #{n}"
      {:ok, fb} = Dict.fetch(state, n)
    else
      Logger.debug "Calculating #{n}"
      fb = fizzbuzz(n)
      state = Dict.put(state, n, fb)
    end
    {:ok, fb , state}
  end

  defp fizzbuzz(n) do
    case {rem(n, 3), rem(n, 5)} do
      {0, 0} -> :FizzBuzz
      {0, _} -> :Fizz
      {_, 0} -> :Buzz
      _      -> n
    end
  end
end

Time to use our code. In the project directory, fire up an iex session in the context of our application with the following command:
iex -S mix

Now we can start a server and compute some values:
FizzBuzz.start_link
4..6 |> Enum.map(&FizzBuzz.print/1)

Here’s the output generated by that command. Due to the asynchronous nature of the requests the outout, log messages and the function’s return value are interleaved:
4

10:54:58.026 [debug] Calculating 4
Buzz
[:ok, :ok, :ok]

10:54:58.028 [debug] Calculating 5
Fizz

Note how despite running in a separate process, the output of IO.puts happened in our iex session, since that’s the current group leader.

If we now try to fetch an already computed value (FizzBuzz.get(5)), we can see that it’s actually retrieved from the cache:
10:58:56.774 [debug] Fetching 5
:Buzz

Summary

In this post we explored how easy it is to implement server applications by leveraging OTP. Our FizzBuzz server is now ready to be deployed as part of a bigger Elixir application, offering features like supervision and hot code swapping.

Resources
Like 235 likes
Michael Kohl
Share:

Join the conversation

This will be shown public
All comments are moderated

Comments

Aman
November 14th, 2021
Getting error
variable "fb" does not exist and is being expanded to "fb()", please use parentheses to remove the ambiguity or change the variable nameElixir

https://prnt.sc/1zjm8n3
https://prnt.sc/1zjmaku

Get our stories delivered

From us to your inbox weekly.