Erlang

Introduction

Initially developed for telecommunication applications such as switches, Erlang is a concurrent-oriented functional language that has almost transparent distribution. It runs on an ad hoc VM called BEAM. BEAM offers many features for parallel and distributed systems, including slow performance degradation and fault-tolerance, making it very robust. Erlang has been used in several relevant industrial applications such as WhatsApp, Amazon’s SimpleDB, and Facebook’s chat function. One of Erlang’s core principles is the “let it crash” principle. This principle involves designing the application with a supervision structure in place to handle errors and crashes. When an error occurs, the process responsible for the error is terminated, and supervision processes restart it and other necessary processes. Erlang also supports hot-swapping of code, allowing for updating application code at runtime. Processes running the previous version of the code continue to execute, while new invocations run the updated code. Erlang syntax is heavily influenced by Prolog.

Integers

Integers in Erlang are used to denote whole numbers (positive or negative). In Erlang there is no maximum size for integers and arbitrarily large whole numbers can be used. It’s also easy to express integers in different bases other than 10 usng the Base#Value notation. If omitted, base 10 is assumed. Operations on integers are standard except that the / operator is used only for floating point division while the div operator is used for integer division.

Atoms

Atoms are like symbols in Scheme: constant literals that stands for themselves. They are similar to enumeration types in other programming languages. If the atom is not enclosed in quotes, valid characters are letters, digits, the AT symbol (@) and the underscore (_). If the atom is enclosed in single quotes, any character is allowed.

Booleans

The atoms true and false are just atoms already defined. The following Boolean operators are available:

  • and
  • andalso
  • or
  • orelse
  • xor
  • not

Variables

Variables in Erlang have some rules like:

  • There is the “Single Assignment”: once you’ve bound a variable, you cannot change its value … so yeah .. it’s not a variable.
  • They start with an uppercase letter, followed by uppercase and lowercase letters, integers and underscores.

Equality

In Erlang, expressions can be compared using the following operators:

== Equal to
/= Not equal to
=:= Exactly equal to
=/= Exactly not equal to
=< Less than or equal to
< Less than
<= Less than or equal to

Tuples

Tuples are used to store a fixed number of items. They can contain any type of data and they are delimited by { } :

> element(1, {martino,piaggi}). 
martino
> element(2, {martino,piaggi}). 
piaggi

Lists

Erlang offers also lists, which are delimited by square brackets (like in Haskell) and their elements are comma-separated. Are very useful for list comprehension. Main lists operations:

  • Using lists:map to square each element in a list:
Squares = lists:map(fun(X) -> X*X end, [1, 2, 3]).

Result: Squares will be [1, 4, 9]

  • Using lists:nth to access elements at specific indices in a list:
Element1 = lists:nth(1, [3, 23, 42]).

Result: Element1 will be 3

Element3 = lists:nth(3,[3 ,23 ,42]).

Result: Element3 will be 42

  • Using lists:member to check if an element is present in a list:
IsMember = lists:member(3,[3 ,23 ,42]).

Result: IsMember will be true

  • Using lists:foldr to apply a function over all elements of a list from right-to-left:
Sum = lists:foldr(fun(X,Y) -> X+Y end, 0 ,[1 ,2 ,3]).

Result : Sum will be 6

  • Using lists_seq:interval/2, which is equivalent to creating a sequence of numbers from start to stop (inclusive):
Sequence = lists_seq:interval(1 ,5).

Result : Sequence will be [1,2,3,4,5]

Pattern Matching

Pattern matching is more expressive than Haskell. Pattern = Expression can be used to:

  • Assign values to variables, for example: {a, b, C} = {a, b, foo}. is a way to bound C variable to atom foo.
  • Control the execution flow of programs
  • Extract values from compound data types

Functions

Erlang function definitions can’t be typed directly in the Erlang shell but they are grouped together in modules. Examples of built-in functions are:

date ()
time ()
size ( {a, b, c})
atom_to_list (an_atom)
integer_to_list (2234)
tuple_to_list ({Y})

Function syntax

  • The arrow -> separates the head of the function from its body.
  • The function can consists of one or more clauses, separated by semicolons ; except for the last one which is terminated by a dot .
  • Each clause defines the action of the function on data which matches the pattern in the head of the clause.
  • Clauses are scanned in order until a match is found.
  • When a match is found, variables in the head of the clause are bound.
  • Variables in each clause are local and are allocated and deallocated automatically.
  • The body of the clause is evaluated sequentially using commas to separate expressions.

Guarded Function Clauses

factorial (0) -> 1;
factorial (N) when N > 0 ->
N * factorial (N - 1) .

Erlang uses guarded function clauses to define functions. A guard is introduced using the keyword when. It is similar to Haskell. The guard sub-language is restricted because it must be evaluated in constant time. This means users cannot use their own predicates while using guards.

Funs

Funs are used to define anonymous functions like lambda in Scheme.

F = fun (Arg1, Arg2, ... ArgN) -> ... End
4> lists:foldl (fun (X,Y) -> X+Y end, 0, [1,2,3]).
6

Funs can be passed as usual to higher order functions: lists:map(Square, [1,2,3] ). returns [1,4,9]. To pass standard functions, we need to prefix their name with fun and state their arity: lists:foldr(fun my_function/2, 0, [1,2,3]).

Apply

apply(Module, Func, Args) applies function Func in module Mod to the arguments in the list Args. Any Erlang expression can be used in the arguments to apply.

-module(apply_example).
-export([add/2]).
 
add(X, Y) ->
    X + Y.
 
main() ->
    Args = [5, 3],
    Result = apply(apply_example, add, Args),
    io:format("Result: ~p~n", [Result]).

If else

Erlang’s syntax does not include a specific keyword for an else clause.

if
	integer(X) -> ... ;
	tuple(X) -> ... ;
	true -> ... % works as an "else"
end,

By convention, it is common practice to end an if expression with the condition true. This ensures that there is always a fallback option in case none of the other conditions match.

Maps

In Erlang, maps are a data structure similar to hash tables which allow you to store key-value pairs. Maps support pattern matching, which makes it easy to extract specific values from them. An example:

% Creating a map using the #{...} syntax
Io = #{name => "Martino", age => 23}.

Result: #{age => 23, name => "Martino"}

% Accessing the entire map
Io.

Result: #{age => 23, name => "Martino"}

% Accessing a specific value from the map using its key
MyName = Io:name.

Result: "Martino"

Other example using maps:

proxyFun(Table) -> 
    receive 
        {remember, Pid, Name} -> 
            proxyFun(Table#{Name => Pid});
        {question, Name, Data} ->
            #{Name := Id} = Table,
            Id ! {question,Data},
            proxyFun(Table);
        {answer, Name, Data} ->
            #{Name := Id} = Table,
            Id ! {answer, Data},
            proxyFun(Table)
    end. 

Message passing

Erlang relies on the “Actor Model”, in which actors (independent unit of computation) can only communicate through messages. Processes are represented using different actors communicating only through messages. Each actor is a lightweight process, handled by the VM: it is not mapped directly to a thread or a system process, and the VM schedules its execution. The syntax ! is used to send anything to any other actor/process:

 
B ! {self(),{mymessage,[1,2,3,42]}}
 
Then in b: 
 
receive
	{A,{mymessage,D}} -> work_on_data(D);
end

With Erlang library you can make very easily and quickly a client-server system. There is construct to make:

sleep (T) ->   
	receive   
		foo -> Actions1 
	after   
	... Time -> Actions2
	end.

When creating a new process in Erlang, it is assigned a process identifier (Pid), which is known only to the parent process. A process can send a message to another process using its identifier.

  • register(Alias,Pid) is a way to make an alias in complex applications.
  • There is also Flush() to clear message queue.

Messages can carry data and provided that the data is applicable, variables can become bound when receiving the message.

Often Message Passing is used with list comprehensions to manage the processes:

% standard map 
map(_, []) -> [];
map(F,[X|Xs]) -> [F(X) | map(F,Xs)].
 
% parallel map
pmap(F, L) ->
    Ps = [ spawn(?MODULE, execute, [F, X, self()]) || X <- L],
    [receive 
         {Pid, X} -> X
     end || Pid <- Ps].
 
execute(F, X, Pid) ->
    Pid ! {self(), F(X)}.

The processes are spawn and organized using lists comprehension.

filterr(F, [H|T]) ->
    case F(H) of
        true  -> [H|filterr(F, T)];
        false -> filterr(F, T)
    end;
filterr(F, []) -> [].
 
%same as pmap but we have to discard
pfilter(F, L) ->
    Ps = [ spawn(?MODULE, execute2, [F, X, self()]) || X <- L],
    lists:foldl(fun (P,Vo) ->
               receive
                  {P,true, X} -> Vo ++ [X];
                  {P,false,_} -> Vo
                end end, [], Ps).
 
execute2(F, X, Pid) ->
    case F(X) of
        true  -> Pid ! {self(), true,X};
        false -> Pid ! {self(), false,X}
    end.

Implementatioon of a parallel foldl where the binary operator F is associative, and N is the number of parallel processes in which to split the evaluation of the fold.

partition(L, N) ->
    M = length(L),
    Chunk = M div N,
    End = M - Chunk*(N-1),
    parthelp(L, N, 1, Chunk, End, []).
 
parthelp(L, 1, P, _, E, Res) ->
    Res ++ [lists:sublist(L, P, E)];
parthelp(L,N,P,C,E ,Res) ->
    R = lists:sublist(L,P,C),
    parthelp( L,N-1,P+C,C,E ,Res ++ [R]).
 
parfold(F,L,N)->
   Ls= partition( L,N ),
   W=[spawn(?MODULE,dofold,[self(),F,X]) || X <-Ls],
   [R|Rs]=[receive {P,V}->V end||P<-W],
   lists:foldl(F,R,Rs).
 
dofold(Proc,F,[X|Xs])->
     Proc ! {self(),lists:foldl(F,X,Xs)}.

A function which takes two list of PIDs [x1, x2, ...], [y1, y2, ...], having the same length, and a function f, and creates a different “broker” process for managing the interaction between each pair of processes xi and yi. At start, the broker process i must send its PID to xi and yi with a message {broker, PID}. Then, the broker i will receive messages {from, PID, data, D} from xi or yi, and it must send to the other one an analogous message but with the broker PID and data D modified by applying f to it.

A special stop message can be sent to a broker i that will end its activity sending the same message to xi and yi.

broker(X,Y,F) ->
    X ! {broker,self()},
    Y ! {broker,self()},
    receive
        {from,X,data,D} ->
            Y ! {from,self(),data,F(D)},
            broker(X,Y,F);
        {from,Y,data,D} ->
            X ! {from,self(),data,F(D)},
            broker(X,Y,F);
        stop ->
            X ! stop,
            Y ! stop,
            ok
    end.
twins([],_,_) -> ok;
twins([X|Xs],[Y|Ys],F) -> spawn(?MODULE,broker,[X,Y,F]), twins(Xs,Ys,F).

Timeouts

In this way we can build sleep function:

sleep (T) ->   
	receive   
	after   
		T -> true   
	end.

Alarm example

setAlarm(T,What)->
    spawn(?MODULE,set,[self(),T,What]),
    receive
        {Alarm} -> io:format("~p~n", [Alarm])
    end.

This will print out the contents of Alarm using Erlang’s format string syntax (~p) and then add a newline character (~n). This way, when you call setAlarm(100,ciao)., it will print out “ciao” instead of “ok”.

set(Pid,T,Alarm) ->
    receive
    after 
        T -> Pid ! {Alarm}
    end.

Managing errors

In the given Erlang code, errors are managed using pattern matching and conditional statements.

master_loop(Count) ->
    receive
        {value, Child, V} ->
            io:format("child ~p has value ~p ~n", [Child, V]),
            Child ! {add, rand:uniform(10)},
            master_loop(Count);
        {'EXIT', Child, normal} ->
            io:format("child ~p has ended ~n", [Child]),
            if
                Count =:= 1 -> ok; % this was the last
                true -> master_loop(Count-1) % works as an "else"
            end;
        {'EXIT', Child, _} -> % "unnormal" termination
            NewChild = spawn_link(?MODULE, child, [0]),
            NewChild ! {add, rand:uniform(10)},
            master_loop(Count)
    end.
 
child(Data) ->
    receive 
        {add,V} ->
             NewData = Data+V,
             BadChance = rand:uniform(10) < 2,
             if 
                 % random error in child:
                 BadChance -> error("I’m dying");
                 % child ends naturally:
                 NewData > 30 -> ok;
                 % there is still work to do:
                 true -> the_master ! {value,self(),NewData},
                         child(NewData)
             end 
     end.

In the master_loop/1 function, there are two cases where errors can occur:

  • when a child process terminates normally ({EXIT, Child, normal})
  • when a child process terminates abnormally ({EXIT, Child, _}).

Overall, errors in this code are handled by throwing an error message when a random condition is met in the child process, and by replacing terminated child processes with new ones in the master process.

Shell commands

Please note that in Erlang shell every instruction must be terminated by a dot . . f() is used to “forget” all possible value bounds to the assignments. f(Variable) if you want to unbound a specific variable.

Good to start: https://www.tryerlang.org/ Learn you some Erlang