Erlang – My first functional language
My first introduction to functional programming was a university course simply called “Programming Languages”. At that time I already used quite a few programming languages and I then believed to them be very different. I therefore assumed the class would be really easy. That did not turn out to be the case, however. Faced with variables that didn’t vary, folds, maps and no for loops, I was struggling for months until it finally, gradually started to make sense. It was even… elegant, and beautiful.
The language we used for the course was the Oz programming language, which worked nicely for teaching purposes, but it did not seem to be widely used. However, one of the last lectures was a guest lecture by a Swedish Erlang programmer, and having seen a language that was actually used, I decided that I should learn Erlang to explore more of this new, strange world of functional programming.
I picked up “Programming Erlang” from the library and started to play around with Erlang. I remember finding the syntax quite weird and the programming model was different from anything I had seen before, but it was fun to play around with. However, the my dalliance with Erlang ended when a friend suggested that I should look at Haskell as well if my goal was to learn about functional programming. That was just before Christmas, so during my Christmas holiday I started reading the Haskell wiki book. Haskell – even with its idiosyncrasies – was pure beauty and elegance, and the type system was more powerful than anything I had seen before. But enough about Haskell. This post is about how I recently started looking at Erlang again.
Revisiting Erlang
I picked up “Introducing Erlang” by Simon St. Laurent last week, which turned out to be a great, quick introduction to Erlang and OTP1. The Erlang syntax didn’t bother me at all this time (I remember finding it rather strange the first time I looked at it), and playing around by writing small services was a lot of fun. I especially liked learning about OTP which is (1) a set of libraries, (2) behaviours (basically “interfaces” for Erlang) and (3) some conventions on how to write services.
One of the most commonly used OTP behaviours is gen_server
(general server).
To use this behaviour you would specify that your program behaves like a
gen_server
by adding the pragma -behaviour(gen_server).
to your
source file. By doing this you tell the compiler that you intend to adhere to
this behaviour, which in practice means implementing a specific set of
functions. (This will be checked by the compiler.) The code for a process is
thus split into a behaviour (a part of OTP) and a callback module that
will implement the application-specific parts.
There is a great post to the erlang-quesitons
list from
2011 by Joe Armstrong, Erlang’s creator, explaining how the gen_server
OTP
behaviour works behind the scenes.
Besides some initialization work (basically registering under a given name and
then calling back to the callback module’s init
function), a gen_server
runs
an infinite, tail-recursive function that calls itself, explicitly passing state
from one call to the next. Its initial state is what’s returned by the calling
module’s init
function. This looping function will wait for messages and
whenever it receives a message it will call the callback module’s handle_call
function and update its state to what was returned by that function.
The following mini_gs
module is used to illustrate how gen_server
works
behind the scenes2:
-module(mini_gs).
-export([start_link/4, call/2]).
%% this module behaves just like the gen-server
%% for a sub-set of the gen_server commands
start_link({local,Name}, Mod, Args, _Opts) ->
register(Name, spawn(fun() -> start(Mod, Args) end)).
% a call to the mini_gs server “Name” is passed on to that server, and we then
% wait for its reply and return it. `make_ref` returns a unique id, so that we
% can tell the messages apart.
call(Name, X) ->
Name ! {self(), Ref = make_ref(), X},
receive
{Ref, Reply} -> Reply
end.
% call the callback module's `init` function and use its return value as our
% initial state
start(Mod, Args) ->
{ok, State} = Mod:init(Args),
loop(Mod, State).
% loop forever, passing calls on to the callback module's `handle_call`
% function, update our state, and pass it on to the recursive call.
loop(Mod, State) ->
receive
{From, Tag, X} ->
case Mod:handle_call(X, From, State) of
{reply, R, State1} ->
From ! {Tag, R},
loop(Mod, State1)
end
end.
So that would be the behaviour. And the following key-value server is an example of a callback module:
-module(kv).
% -behaviour(mini_gs). % would work if `mini_gs` were a part of OTP
%% These define the client API
-export([start/0, store/2, lookup/1]).
%% these must be defined because they are called by mini_gs
-export([init/1, handle_call/3]).
%% define the client API
start() -> mini_gs:start_link({local,someatom}, kv, foo, []).
store(Key,Val) -> mini_gs:call(someatom, {putval,Key,Val}).
lookup(Key) -> mini_gs:call(someatom, {getval,Key}).
%% define the internal routines
init(foo) -> {ok, dict:new()}. % The initial state is an empty dictionary
handle_call({putval, Key, Val}, _From, Dict) ->
{reply, ok, dict:store(Key, Val, Dict)};
handle_call({getval,Key}, _From, Dict) ->
{reply, dict:find(Key, Dict), Dict}.
Why this is so cool is best explained by Joe Armstrong himself:
So now we have turned a single process key-value store (using
dict
) into a global key-value store. Note thatkv.erl
never uses the primitivesspawn_link
,send
,receive
or so on. i.e.kv.erl
is written with pure sequential code.This is why we made the
gen_server
abstraction. You can write well-typed sequential code (thehandle_call
andinit
functions) to parametrize a concurrent behavior, i.e. you need to know nothing about concurrency to get the job done. We’ve “abstracted out” the concurrency.
(If this explanation was too terse, I recommend Steve Vinoski’s talk on OTP from QCon New York 2012. Actually, watch it anyway – it’s a great talk.)
(Not really a) conclusion
So, to summarize a bit: What I have seen so far in my recent re-visit of Erlang has been really positive. Maybe most important of all: Playing around with it has been fun! There are definitely some rough spots in the language, like the lack of a real type system3; a weak record system; and the slightly annoying issue of having to change statement terminators (i.e. “.”, “,” and “;”) when moving code around, but being new to the language and the ecosystem, I’m not even sure they will be valid complaints as I learn more.