Saturday, 26 April 2008

Erlang for the OO-minded

Somehow my thoughts about concurrency-oriented programming as a better way of object-orientation seem to jolted the community. I never had so much traffic on my site. The discussion has been undecided. Some had problems with the functional nature of erlang, other ones with the single-assignment of variables in Erlang. Those who understood the ideas behind the Erlang messaging and the generic server have been able to follow me. So I'll try to make it clearer.

So let's take a small - and useless *smile* - Java class:

public class Adder {
private int a, b;

public void setA(int anA) {
a = anA
}

public void setB(int aB) {
b = aB
}

public int result() {
return a + b
}
}

The usage would be really simple:

Adder myAdder = new Adder();

myAdder.setA(1);
myAdder.setB(2);

System.out.println(myAdder.result());

And now the same thing in Erlang. I will show it in two different ways. The first one works without processes. It is simple, straight foreward and can be realized this way in many languages. But it doesn't use the advantages.

create() ->
{0, 0}.

set_a(A, {_oldA, B}) ->
{A, B}.

set_b(B, {A, _oldB}) ->
{A, B}.

result({A, B}) ->
A + B.

If the module of this code is called adder the usage would be

A1 = adder:create(),

A2 = adder:set_a(1, A1),
A3 = adder:set_b(2, A2),

io:format("~w", [adder:result(A3)]).

That's no real object-oriented way, it only shows the way how many Erlang modules work, e.g. to create and manage dictionaries. The data is managed using the Erlang basic and higher level data types like tuples and lists. Every change creates a new data or data structure because variables in Erlang are only single-assignable. Beside optimization the the major reason is the prevention of side-effects. Later more about that.

The implementation as a process looks a bit different:

create() ->
spawn(?MODULE, loop, [0, 0]).

set_a(Pid, A) ->
Pid ! {set_a, A}.

set_b(Pid, B) ->
Pid ! {set_b, B}.

result(Pid) ->
Pid ! {result, self()},
receive
{response, Value} -> Value
end.

loop(A, B) ->
receive
{set_a, newA} ->
loop(newA, B);
{set_b, newB} ->
loop(A, newB);
{result, Pid} ->
Pid ! {response, A + B}
loop(A, B)
end.

The major parts are the creation of the process using spawn and the process function loop. Inside this function messages are received and processed. After that the function is called tail-recursive, there'll be no stack-overflow. The status of the process - in object-oriented languages called attributes, instance variables, or properties - are maintained in the arguments A and B. Alternatively of several single arguments one tuple of record containing a complex data structure can be used. The other functions above are just helper functions, especially the result function. This is due to the asynchronous message handling where the return of the result is also a message send and receive. So the usage will be

Pid = adder2.create(),

adder2:set_a(Pid, 1),
adder2:set_b(Pid, 2),

io:format("~w", [adder2:result(Pid)]).

Here you easily see how this time only one object - the process - is created and modified. Due to the fact that a process has only one message queue all messages are handled sequentially. So there're no problems with synchronizations, semaphores, or locks. Multiple processes can use this process with no problems. And here the old metaphor of sending messages to objects is really true. You may think "Nah, doing dispatching on my own, bullshit." But OTP modules like gen_server and the callback mechanism allow to simplify that. They are generic, like abstract classes, and provide all the needed stuff so that you can concentrate on the business logic and some comfort functions.

So where's the advantage? Surely not in those tiny processes, I would implement them as standard modules like the first example. But the strength of Erlang is the concurrency, the parallel processing. Spawned processes are not working sequentially but really parallely, on one core, on multiple cores, on multiple processors, and on multiple systems. And that's the big advantage. Think about a special architecture like pipes and filters for the processing of a larger amount of data.


In a typical sequential way each retrieved insurance holder would be processes step by step and typically on a single processor. The processing of a large number of insurance holders and their contracts would last a long time. One solution could be the usage of multi-threading for the filters together with synchronized data queues for the pipes. Using inheritance simplifies the implementation. This solution would use multiple cores and processors. But still there's a limitation in the distribution of the filters or groups of filters. For example everything up to the premium filter could be on a first system, the both branches behind it on two further systems. With most languages you would need special pipes for the remote communication, which again would make the whole solution more complex.

To distribute processes in Erlang it would be just necessary to add another function:

create(Node) ->
spawn(Node, ?MODULE, loop, [0, 0]).

This way the adder - or in the scenario above a filter - could be startet on a different node and be used like if it is working locally.

Pid = adder2.create(my_node@my-server.in.my.net)

Beside that there's no need for more implementation. Only the VMs have to be started with a name, a cookie for securing the networking, and a host file with the names of all the nodes. It's funny how simple it is. The example above also shows how Erlang handles polymorphism. One way is the arity of the functions. That's the reason why the export of functions also contains the number of arguments. Here's one small example defining a function in the adder module.

-module(adder).
-export([add/1]).

add(List) ->
add(List, 0).

add([Head|Tail], Acc) ->
add(Tail, Acc + Head);
add([], Acc) ->
Acc.

Only the add function with one argument will be exported, the other one is internally. It shows also the second way of polymorphism, the pattern-matching. While there are elements in the list the first of the two add functions with two arguments will be executed. It adds the head element to the accumulator and continues recursively with the tail. If the list is empty the second one is called, which returns the accumulator as the result. Beside the functions the pattern-matching also works in the case-, if-, and receive-statements. I've shown this already in the adder process function above. It's easy to see how the received tuples could contain the same command atom as the first element and then a different number of arguments as the further elements.

Another way to realize polymorphism are guards. Those are constraints which can be added to function definitions and pattern-matchings. One major task of guards is to do type checking. Erlang uses duck typing, so the arity is sometimes not enough, e.g. for a function to append anything in it's string representation to a string.

string_append(String, Float) when is_float(Float) ->
...;
string_append(String, Integer) when is_integer(Integer) ->
...;
string_append(String, Tuple) when is_tuple(Tuple) ->
...

Multiple guards can also be combined using a semicolon (or) and a comma (and). They will be evaluated short-circuited to increase the performance. Their flexible definition additionally allows a more powerful polymorphism than in traditional languages. Think about a process for withdrawals which shall do this differently for different amounts:

loop(State) ->
receive
{withdraw, Amount, Account, Lo, Hi} when Amount =< Lo ->
% Perform a standard withdraw.
...;
{withdraw, Amount, Account, Lo, Hi} when Amount > Hi ->
% Perform a special customer approval before the withdraw.
...;
{withdraw, Amount, Account, Lo, Hi} ->
% Perform a simple customer approval for withdrawals between lo and hi.
...
end.

One big part of object-orientation is still missing: the inheritance. Here Erlang has no real solution in the sense of deep hierarchies based on one root class. But with behaviours and callbacks you can at least realize something like abstract classes and their children. The OTP libraries use this for several powerful modules. Here's my very small implementation of the generic server. The original one is by far more sophisticated.

-module(server).
-export([start/2, stop/1, call/2]).

start(Module, Args) ->
% Call init/1 in Module.
% It has to return an initial state.
State = Module:init(Args),
spawn(?MODULE, loop, [Module, State]).

stop(Pid) ->
Pid ! stop,
ok.

call(Pid, Msg) ->
Pid ! {call, Msg, self()},
receive
{response, Value} -> Value
end.

loop(Module, State) ->
receive
{call, Msg, Pid} ->
% Call function handle/2 in Module.
% It has to return {Value, NewState}.
{Value, NewState} = Module:handle(Msg, State),
Pid ! {response, Value},
loop(Module, NewState);
stop ->
% Call function terminate/1 in Module.
Module:terminate(State)
end.

This way the developer just has to implement the three functions init/1, handle/2 for each message, and terminate/1.

-module(account_server).
-export([init/1, handle/2, terminate/1]).

init(Args) ->
% Create an initial state, e.g. a database connection.
...

handle({open, Account}, State) ->
...,
{0, NewState};
handle({withdraw, Amount, Account}, State) ->
...,
{Balance, NewState};
handle({deposit, Amount, Account}, State) ->
...,
{Balance, NewState};
handle({balance, Account}, State) ->
...,
{Balance, NewState}.

terminate(State) ->
...,
ok.

So a simple session could be:

Pid = server:start(account_server, DatabaseName),

server:call(Pid, {open, 4711}),
server:call(Pid, {deposit, 1000.0, 4711}),
server:call(Pid, {withdraw, 250.0, 4711}),

Balance = server:call(Pid, {balance, 4711}),

% Balance now should be 750.0.

server:stop(Pid).

As written above this is typically more powerful and elegant handled, but this example should be enough to let you understand how Erlang processes could be seen as a kind of objects. Additionally to the features I mentioned here the receive construct also knows a time-based action which is called when no message has arrived for a given time. And through a simple mechanism parent processes can be notified if a child dies. These both features again allow more powerful solutions. Maybe this is reason enough for you to be as interested as I am in developing with Erlang.

2 Comments:

S9 said...

Good Article. I am a big fan of the Erlang language and OTP.

I also have been trying to explain to C# developers how easy it is to write clear, consistent, multi-process applications in Erlang.

I came from years of development in OO and Structured programming languages. After reading Joe Armstrong's book I felt like I was "armed" with a new set of weapons.

A piece of advice to anyone reading this article: "Once you step into this world, you will not look back, so get ready for a fun trip".

Dein Bär said...

Hi, my original comment turned out to be quite long. So I turned it into an article. :-)