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
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