Erlang Tutorial — Part 1A

John J. Camilleri
TDA384/DIT391 Principles of Concurrent Programming
HT2017 LP1

Erlang

Resources

Syntax

Erlang shell

Hello world

% file: foo.erl
-module(foo).
-export([hw/0, hw/1]).

hw() -> % hw/0
  Msg = "Hello world!\n",
  io:fwrite(Msg).

hw(Name) -> % hw/1
  Msg = "Hello " ++ Name ++ "!\n",
  io:fwrite(Msg).

Typing is dynamic

Tuples

Case

P1 = {human, "John", "Camilleri", 30},
P2 = {dog, "Fluffy", 3},
% Age = 17, % enabling this leads to no match
case P1 of
  {human, _, _, Age} -> Age ;
  {dog, _, Age} -> Age * 7 ;
  _ -> unknown_age
end.

Pattern matching in function definitions

% definition of age/1
age({human, _, _, Age}) -> Age ;
age({dog, _, Age}) -> Age * 7 ;
age(_) -> unknown_age.

If

if
  Age > 18 -> adult ;
  Age > 0  -> minor ;
  true     -> invalid_age % else/catch-all
end.

Lists

Strings

Tail recursion

Summing of a list of numbers can be expressed recursively:

sum([X1,X2,X3,X4]) == X1 + X2 + X3 + X4
sum([X1,X2,X3,X4]) == X1 + (X2 + X3 + X4)
sum([X1,X2,X3,X4]) == X1 + sum([X2,X3,X4])

The sum function:

sum([]) -> 0; % base case
sum([X1|XS]) -> X1 + sum(XS). %recursive case

List comprehensions

Alternative syntax for constructing lists by combining mapping and filtering.

Functions

No loops for control flow


Erlang Tutorial — Part 1B

Shared state

class Counter implements Runnable {

  private int x = 0;

  public void run () {
    x++;
  }

  public static void main (String[] args) {
    Counter c = new Counter();
    (new Thread(c, "thread1")).start();
    (new Thread(c, "thread2")).start();
  }

}

Message passing

Spawning

spawn(foo, hw, []).
spawn(fun() -> ... end).
spawn(fun() -> timer:sleep(2000), io:fwrite("Done\n") end).
[spawn(fun() -> io:fwrite("I got the value ~p ~n", [X]) end) || X <- lists:seq(1,5)]

Sending & receiving

% spawn process which waits for a message and prints it
Pid = spawn(fun() ->
  receive
    X -> io:fwrite("I received: ~p~n", [X])
  end
end),
% send a message to the new process
Pid ! {message, "Hello"}.

Pattern matching in receiving

receive
  {greet, Name}         -> "Hello " ++ Name ; % return a string
  {function, Fun, Args} -> apply(Fun, Args) ; % apply a fucntion to some arguments
  X                     -> io:fwrite("I received: ~p~n", [X]) % perform output (returning ok)
end.

Clients and servers

A server is a process which runs forever:

  1. wait for a message from a client (request)
  2. process request
  3. send message back to client (response)
  4. repeat
recv() ->
  receive
    ... -> % receive request
      ... % send response
  end,
  recv(). % loop

A client a process which:

  1. sends a message to server (request)
  2. receives a message back from server (response)
Pid ! ..., % send request
receive
  ... -> % receive response
end.

Returning value to client

Server

% The function which the server will run
recv() ->
  receive
    {Pid, greet, Name}         -> Pid ! "Hello "++Name ;
    {Pid, function, Fun, Args} -> Pid ! apply(Fun, Args) ;
    _ -> skip
  end,
  recv().

% Starting a server process
Pid = spawn(foo, recv, []).

Client

Pid ! {self(), greet, "everyone"},
io:fwrite("Request sent~n"),
receive
  X -> io:fwrite("Server response: ~p~n", [X])
end.

Check waiting messages

self() ! foo,
self() ! bar,
process_info(self(), messages),
receive X -> X end,
X,
receive X -> X end. % this blocks even though there's a message waiting

Server state

Add an argument into server function which is updated with each call

recv(N) ->
  receive
    _ -> ignore
  end,
  io:fwrite("Server has handled a total of ~p requests~n", [N+1]),
  recv(N+1).

Pid = spawn(foo, recv, [0]). % 0 = initial value of server's state

Pid ! "hello",
Pid ! "anybody there?",
Pid ! ":(".

Erlang Tutorial — Part 2

Sequential merge sort

sort([]) -> [] ;
sort([X]) -> [X] ;
sort(Xs) ->
  Len = length(Xs),
  {Part1, Part2} = lists:split(Len div 2, Xs),
  merge(sort(Part1), sort(Part2)).

merge([], Ys) -> Ys ;
merge(Xs, []) -> Xs ;
merge(Xs, Ys) ->
  [XH|XT] = Xs,
  [YH|YT] = Ys,
  if
    XH > YH -> [YH] ++ merge(Xs, YT) ;
    true    -> [XH] ++ merge(XT, Ys)
  end.

Concurrent merge sort

sort(Xs) ->
  Len = length(Xs),
  {Part1, Part2} = lists:split(Len div 2, Xs),
  Pid = self(),
  spawn(fun() -> Pid ! {result, sort(Part1)} end),
  spawn(fun() -> Pid ! {result, sort(Part2)} end),
  receive {result, Res1} -> ok end,
  receive {result, Res2} -> ok end,
  merge(Res1, Res2).

Registering processes

Math server

% Start and register the server process to the atom `math`
start_server() ->
  Pid = spawn(fun() -> math_server() end),
  catch unregister(math),
  register(math, Pid).

% Server
math_server() ->
  receive
    {compute, Fun, Args, From} ->
      spawn(fun() ->
        Ans = apply(Fun, Args),
        timer:sleep(round(Ans)*10), % artificial wait
        From ! {answer, Ans}
      end)
  end,
  math_server().

% Client
power(Num, Exp) ->
  math ! {compute, fun(X,Y) -> round(math:pow(X, Y)) end, [Num, Exp], self()},
  receive
    {answer, Ans} -> io:fwrite("~p ^ ~p = ~p\n", [Num, Exp, Ans])
  end.

Mixing up request and response

powers(Nums, Exp) ->
  Fun = fun(X,Y) -> round(math:pow(X, Y)) end,
  [ math ! {compute, Fun, [Num, Exp], self()} || Num <- Nums ],
  Ans = [ receive {answer, Ans} -> Ans end || _ <- Nums ],
  io:fwrite("~p ^ ~p = ~p\n", [Nums, Exp, Ans]).
% Server
math_server() ->
  receive
    {compute, Fun, Args, From, Ref} ->
      spawn(fun() ->
        Ans = apply(Fun, Args),
        timer:sleep(round(Ans)*10),
        From ! {answer, Ref, Ans}
      end)
  end,
  math_server().

% Client
powers(Nums, Exp) -> % e.g. Nums = [4,2,8], Exp = 2
  Refd = [ {make_ref(), Num} || Num <- Nums ], % Refd = [{#Ref<.>, 4}, {#Ref<.>, 2}, {#Ref<.>, 8}]
  Fun = fun(X,Y) -> round(math:pow(X, Y)) end,
  [ math ! {compute, Fun, [Num, Exp], self(), Ref} || {Ref, Num} <- Refd ], % Send messages, ignore return values
  Ans = [ receive {answer, Ref, Ans} -> Ans end || {Ref, _} <- Refd ], % Ans = [16,4,64]
  io:fwrite("~p ^ ~p = ~p\n", [Nums, Exp, Ans]).

Generic server

% genserver.erl
-module(genserver).
-export([start/3, request/2]).

%% Spawn a process and register it with a given atom
%% Function F should have arity 2
start(Atom, State, F) ->
  Pid = spawn(fun() -> loop(State, F) end),
  catch(unregister(Atom)),
  register(Atom, Pid),
  Pid.

loop(State, F) ->
  receive
    {request, From, Ref, Data} ->
      {reply, R, NewState} = F(State, Data),
      From!{result, Ref, R},
      loop(NewState, F)
  end.

%% Send a request to a Pid and wait for a response
request(Pid, Data) ->
  Ref = make_ref(),
  Pid!{request, self(), Ref, Data},
  receive
    {result, Ref, Result} ->
      Result
  after 3000 ->
    exit("Timeout")
  end.

Implementation of math server using genserver

% Start our server
start_server() ->
  genserver:start(math, nostate, fun math_server/2).

% Server message handling function (private)
math_server(State, {compute, Fun, Args}) ->
  Ans = apply(Fun, Args),
  {reply, Ans, State}.

% Client
powers(Nums, Exp) ->
  Fun = fun(X,Y) -> round(math:pow(X, Y)) end,
  Ans = [ genserver:request(math, {compute, Fun, [Num, Exp]}) || Num <- Nums ],
  io:fwrite("~p ^ ~p = ~p\n", [Nums, Exp, Ans]).

In this example we have removed asynchronicity (the client waits for the first result before sending the second computation).

Make it asynchronous again

% Start our server
start_server() ->
  genserver:start(math, nostate, fun math_server/2).

% Server
math_server(State, {compute, Fun, Args, From}) ->
  Ref = make_ref(), % create a reference for this computation
  spawn(fun() -> % put computation in new process
    Ans = apply(Fun, Args),
    timer:sleep(round(Ans)*10),
    From ! {answer, Ref, Ans} % new process sends directly to client
  end),
  {reply, Ref, State}. % return reference to client immediately

% Client
powers(Nums, Exp) ->
  Fun = fun(X,Y) -> round(math:pow(X, Y)) end,
  Refs = [ genserver:request(math, {compute, Fun, [Num, Exp], self()}) || Num <- Nums ], % send all computations, receiveing references
  Ans = [ receive {answer, Ref, Ans} -> Ans end || Ref <- Refs ], % receive results in correct order
  io:fwrite("~p ^ ~p = ~p\n", [Nums, Exp, Ans]).

Records

-record(person, {name="", born}). % Define the record type for compiler. Default values are optional.

X = #person{name="John", born=1986}), % Converted to: {person,"John",1986}
Name = X#person.name. % Name == "John"
X2 = X#person{name="Mary"}. % X2 == {person, "Mary", 1986}

Exercise

Consider the code below (names have been obfuscated).
Remember, hd/1 gives the head of a list and tl/1 gives the tail.

s(Xs) ->
  receive
    {alpha, X} -> s([X|Xs]) ;
    {beta,  P} -> P ! hd(Xs), s(tl(Xs))
  end.

demo() ->
  Pid = spawn(fun() -> s([]) end),
  Pid ! {alpha, "hello"},
  Pid ! {alpha, 467},
  Pid ! {alpha, {person, "Mary"}},
  Pid ! {beta, self()},
  receive
    Something -> io:fwrite("I received: ~p\n", [Something])
  end.
  1. What is the function s/1 modelling?
  2. What will happen when running demo/0? Will there be any output or errors?
  3. Are there any potential problems with our implementation? How would you fix them?