Due to my interest in concurrent and parallel programming I recently started investigating the Erlang programming language, reading and practicing the book by Joe Armstrong "Programming Erlang, Software for a Concurrent World" (the book is awesome, easy and involving writing style, or in editor’s words: "Joe I want you to imagine you’re sitting at a terminal with your friend beside you, explaining how things work" - more on the book). Erlang is a concurrency oriented programming language (based on a mix between actors and functional paradigms) meaning you get first class (and really well engineered) abstractions for process and process comunication, pattern matching, higher order functions, and absolute lack of shared variables, processess communicate by (asynchronous) message passing. Coming from an object oriented (imperative) programming language (e.g. Java, C#) Erlang just feels kind of esoteric and alien, but having a sound Prolog background you can advance in the process of learning quite easily (time to express some deep appreciation for prof. Viroli’s Prolog lessons from "Computational Langauges and Models" graduate course). To have a quick grasp of the language and of some features I value most, let’s take a look at the code that realizes a counter in Erlang (who doesn’t love counters?
). Personal note: I found the "Hello Counter" a really nice example to start exploring the computational landscape of a programming paradigm; it’s basic and quite universal (it resembles a Minsky Machine). This is a function modelling a cyclic counter:
counter(Value, EndVal) ->
NewValue = receive
inc ->
(Value + 1) rem EndVal;
{get_value, PID} ->
PID ! {value, Value},
Value
end,
counter(NewValue, EndVal).
The function has two arguments:
- Value represents the state of the counter and it is passed through the (tail) recursive function call;
- EndVal is the limit for the cyclic counter;
and receives two kind of messages, through the use of the communication primitive receive (which is blocking, i.e. it stops the execution flow until a message matching one of the specified patterns is received):
- an inc atom which causes the increment of the counter value argument of the recursive function call. Here you have to keep in mind that receive evaluates to the value of the expression following the -> of the pattern matching the message. The expression following the inc pattern is the expression that represent the next value of a cyclic counter: value(n+1) = (value(n) + 1) % limit rem is the infix operator for the modulo operation. Hence, in the case an inc message is received, the variable NewValue is unified with the value of the previous expression, i.e. the counter increments;
- a {get_value, PID} tuple where get_value is an atom and PID is a variable unifying with the second field of the tuple, the identifier of the process requesting the current counter value. Here the expression following the -> is made of two expressions: in the first one the current counter value is sent (through the ! operator) to the requesting process in a tuple of the kind {value, Value}, the second simply evaluates to the current counter value, which NewValue is unified to (otherwise NewValue would have been unified with {value, Value}, being that the last expression). A note on !: contrary to receive it is not blocking, hence if you want to create a rendezvous you have to send a request message and then wait for an answer message.
The last expression is a tail recursive function call, whith NewValue as value of the counter. Now, after saving the code in the counter module, a counter process can be spawned using the the counter function as the process body (the variable CounterP is unified to the process identifier of the spawned process):
CounterP = spawn(counter, counter, [0,42])
incremented by sending a message:
CounterP ! inc
and questioned on its current value (the self() function evaluates to the process identifier of the calling process):
CounterValue = CounterP ! {get_value, self()}
The cyclic counter here defined can be quite puzzling at first (for those who come from an OO background) but has some interesting properties about concurrency and distribution due to the Erlang programming model/platform, hereafter one to cath them all: this program is yet ready to be run as part of a concurrent/distributed system. As a proof of concept to this bold statement let’s implement an auto-increment controller (optionally remote) for the cyclic counter (auto-increment controller is a pompous definition for introducing a start/stop auto-increment cycle). The controller can be thought as another process incrementing the counter on a given time interval, switched on/off by sending it start/stop messages. Through the use of pattern matching in function definition different behaviour can be distinguished, id est the function auto_counter can have multiple clauses for different patterns of its arguments, as follows:
auto_counter(active,…) -> incrementig the counter every delta time until a stop message is received auto_counter(inactive,…) -> doing nothing until a start message is received
Here it follows the actual code for the (remote) auto-increment counter controller (the do X until a Y message is received is modelled through the use of a complete receive clause which comprises after T -> Expression):
auto_counter(active, CounterP, DeltaT) ->
Activity = receive
stop -> inactive
after DeltaT ->
CounterP ! inc,
active
end,
auto_counter(Activity, CounterP, DeltaT);
auto_counter(inactive, CounterP, DeltaT) ->
receive
start -> auto_counter(active, CounterP, DeltaT)
end.
The system just developed is "distribution ready": the counter process and the auto-increment process can be in the same Erlang runtime, in different runtimes on the same machine, in different machines, no change in their definition is needed to go through these different scenarios. This does not happen because the distribution/concurrency of the system is hide by some magic trickery trying to over-smart the programmer in believing remote entities could be considerated as near as local (the Fallacies of Distributed Computing), but it does happen because Erlang builds on the assumption that (from "Programming Erlang - Sofware for a Concurrent World":
The world is parallel If we want to write programs that behave as other objects behave in the real world, then these programs will have a concurrent structure.
This is just a small taste of Erlang and there is much more in the reliance/healing/scalability capabilities (granted by OTP), however it should be clear that: high-level abstractions are offered (and needed) to capture in a syntethic and powerful manner problems: communication primitives, pattern matching, first-order functions, list comprehensions; (very cheap and lightweight) Processes are first-class citizens, and systems are "naturally" modelled as concurrent; avoiding side effects (as long as it is possible… more on this in next posts) leads easens scalabilty and load capability. A last note on the presented program, the auto_counter function does some over-restrictive assumption on the sequence of messages it receives (exercise left to the reader

: try to figure out what the problem is) tomorrow a solution will be proposed. Ending a special thank to
prof. Alessandro Ricci and
Giulio Piancastelli for the tolerance shown towards my tendency to "express" (face-to-face/online) every random thougth about concurrency happening to strike my brain.
(more…)