Erlang
Functional
Concurrent
Distributed
“Soft” real-time
OTP (fault-tolerance, hot code update...)
Open source
We cover the following aspects. Click on the links for further information.
Data types and variables (Erlang manual)
Function definitions (Erlang manual)
Pattern matching (Erlang manual)
Evaluation strategy
Tail recursion (Learn some Erlang for Great Good!)
Numbers
Atoms
start_with_a_lower_case_letter ’Anything_inside_quotes\n\09’
Tuples
{} {atom, another_atom, ‘PPxT’} {atom, {tup, 2}, {{tup}, element}}
[] [65,66,67,68] = “ABCD” % bad practice (experts only) [1, true] [1 | [true] ]
-record(person, {name = “”, phone = [], address}).
X = #person{name = “Joe”, phone = [1,1,2], address= “here”}
X#person.name X#person{phone = [0,3,1,1,2,3,4]}
data Tree a = Leaf a | Node (Tree a) (Tree a)
edoc style “types”
Example:
Node (Node (Leaf 3) (Leaf 4)) (Leaf 5)becomes
{node, {node, {leaf,3}, {leaf, 4}}, {leaf, 5}}
A_long_variable_name
;
.
(often forgotten)factorial(0) -> 1; factorial(N) -> N * factorial(N-1).
Eager evaluation strategy
Example
factorial(3)It applies the definition of
factorial(N)
with N = 3
(second clause), thus
factorial(3) == 3 * factorial(3 - 1)
It now evaluates the argument of the function *
,
which implies evaluating factorial(3 - 1)
. For that,
Erlang firstly evaluates the argument of the function
(eager evaluation strategy).
factorial(3) == 3 * factorial(2)
As before, Erlang applies the second clause with N = 2
.
factorial(3) == 3 * (2 * factorial (2 - 1))
Erlang then reduces the argument of factorial
.
factorial(3) == 3 * (2 * factorial (1))
Now, the second clause with N = 1
gets applied as follows.
factorial(3) == 3 * (2 * (1 * factorial (1 - 1))
Finally, we obtain
factorial(3) == 3 * (2 * (1 * factorial (0))
and Erlang can then apply the first clause.
factorial(3) == 3 * (2 * (1 * 1))
By definition of *
, we have that
factorial(3) == 6
as expected.
The factorial
definition uses pattern matching over numbers
A more involved example.
Function area
to compute the area of different geometrical
figures.
area({square, Side}) -> Side * Side ; area({circle, Radio}) -> Radio*Radio*math:pi().
Patterns: {square, Side}
and {circle, Radio}
{square, Side}
matches {square, 4}
and binds 4
to
variable Side
{circle, Radio}
matches {circle, 1}
and binds 1
to
variable Radio
{B, C, D} = {10, foo, bar}
B
to 10
, C
to foo
and D
to
bar
{A, A, B} = {abc, abc, foo}
A
to abc
, B
to foo
{A, A, B} = {abc, def, 123}
[A,B,C,D] = [1,2,3]
[H|T]= [1,2,3,4]
H
to 1
, T
to [2,3,4]
[H|T] = [abc]
H
to abc
, T
to []
[H|T] = []
{A, _ , [ B | _ ] , {B} } = {abc,23,[22,x],{22}}
A
to abc
, B
to 22
Basic compilation unit is a module
Modules contain function definitions
factorial
and area
(see above)Let us create the module math_examples
as follows.
-module(math_examples). -export([factorial/1,area/1]). factorial(0) -> 1; factorial(N) -> N * factorial(N-1). area({square, Side}) -> Side*Side ; area({circle, Radio}) -> Radio*Radio*math:pi().
Running the examples.
> c(math_examples). {ok,math_examples} > math_examples:factorial(3). 6 > math_examples:area({square,4}). 16 > math_examples:area({circle,1}). 3.141592653589793
We want to define different functions working on lists
> c(list_examples). {ok,list_examples} > list_examples:sum([1,2,3,4]). 10 > list_examples:len([0,1,0,1]). 4 > list_examples:append([5,4],[1,2,3]). [5,4,1,2,3]
We will define them recursively (inductively)
[]
)[X | XS]
)Simplest case (base case)
sum([]) == 0
The inductive case
sum([X1,X2,X3,X4]) == X1 + X2 + X3 + X4
We should try to rewrite this equation so that it
appears the head of the list (X1
) and the rest of
the list ([X2,X3,X4]
) applied to sum
. Let us associate
the sum as follows.
sum([X1,X2,X3,X4]) == X1 + (X2 + X3 + X4)
We know that X2 + X3 + X4
is equal to sum([X2,X3,X4])
.
After all, this is the function that we want to define.
We can then rewrite the equation above as:
sum([X1,X2,X3,X4]) == X1 + sum([X2,X3,X4])
X
and the rest of the list is
XS
sum([X | XS]) == X + sum(XS)
sum
function as follows. sum([X|XS]) -> X + sum(XS); sum([]) -> 0.
Why is the base case as second clause?
Computing the length of a list
len([X|XS]) -> 1 + len(XS) ; len([]) -> 0.
X
is not used on the right-hand side. It can
be replaced by _
len([_|XS]) -> 1 + len(XS) ; len([]) -> 0.
Append elements to the end of a list
append([X|XS],YS) -> [X | append(XS,YS) ] ; append([], YS) -> YS.
Programming pattern to increase performance
Inefficient recursive definition
len([_|XS]) -> 1 + len(XS) ; len([]) -> 0.
len([1,2,3])
len([1,2,3]) == 1 + len([2,3]) len([1,2,3]) == 1 + (1 + len([3])) len([1,2,3]) == 1 + (1 + (1 + len([]))) len([1,2,3]) == 1 + (1 + (1 + 0)) len([1,2,3]) == 1 + (1 + 1) len([1,2,3]) == 1 + 2 len([1,2,3]) == 3
At the time of reaching the marked line, Erlang needs to keep in memory a long expression
After that line, it starts shrinking the expression
Imaging how it will work for a very big list!
More efficiency by tail recursion
Space (constant if we assume elements of the list have the same size)
Efficiency (No returns from recursive calls)
What is the trick?
We define len_a
, the tail recursive version of len
Function len_a
has an extra parameters capturing the partial
result of the function, i.e., how many elements len_a
has seen so far
len_a([_|XS], Acc) -> len_a(XS, Acc+1); len_a([], Acc) -> Acc.
We define len
based on len_a
as follows
len(XS) -> len_a(XS, 0).
What about the tail recursive version of sum
and append
?
Homework!
Hint: the partial result for sum
consists on the
addition of all the numbers that the function has seen
so far
Hint: to implement append
think about the tail recursive
version of a function which reverses a list, i.e.,
reverse([1,2,3,4]) = [4,3,2,1]
Records are very convenient in several situations
Sometimes, functions require extra arguments but we realize about that later on
For instance, assume we would like to write the following error reporting function
debug(50) -> io:format("Item not found in the stock ~p",[]) ; debug(100) -> io:format("User not registered ~p",[]) ; debug(ErrCode) -> io:format("Unknown error ~p. Shutting down ~n",[ErrCode]).
Now, you would like to show part of your program state when an unknown error happens for debugging purposes
debug(50,_) -> io:format("Item not found in the stock ~p",[]) ; debug(100,_) -> io:format("User not registered ~p",[]) ; debug(ErrCode,State) -> io:format("Unknown error = ~p State = ~p ~n",[ErrCode, State]),
If the function debug
had 100 cases, you need to modify 100 lines!
Use records for those arguments that might get extended in the future!
-record(arg, { number }). debug(#arg{ number = 50}) -> io:format("Item not found in the stock ~p",[]) ; debug(#arg{ number = 100}) -> io:format("User not registered ~p",[]) ; debug(#arg{ number = ErrCode}) -> io:format("Unknown error ~p. Shutting down ~n",[ErrCode]).
If we want to add the state to print out, it is easy: modify the definition
of arg
and only the line that uses that extra information.
-record(arg, {number, state}). debug(#arg{number = 50}) -> io:format("Item not found in the stock ~p",[]) ; debug(#arg{number = 100}) -> io:format("User not registered ~p",[]) ; debug(#arg{number = ErrCode, state = State}) -> io:format("Unknown error ~p. Shutting down ~n",[ErrCode, State]).
It is possible to refer to the whole register passed as argument
-record(arg, {number, state}). debug(#arg{number = 50}) -> io:format("Item not found in the stock ~p",[]) ; debug(#arg{number = 100}) -> io:format("User not registered ~p",[]) ; debug(Arg = #arg{number = ErrCode, state = State}) -> io:format("Unknown error ~p. Shutting down ~n",[ErrCode, State]), io:format("The function was called with the record ~p~n",[Arg]),
Pattern matching happens at the level of the argument (it is a record) and the record's components
This would be particularly helpful when doing the laboration CCHAT
Processes
Message passing
Reacting to multiple messages
No share data!
Process is essentially another function running concurrently.
start() -> Pid = spawn(fun run/0), io:format("Spawned ~p~n",[Pid]). run() -> io:format("Hello!~n",[]).
The ,
is sequential composition (like ;
in Java)
Expression fun run/0
indicates to execute the function
run
which has zero arguments.
Function spawn
returns the proceed Id (Pid
) of the newly
created process for future interaction with it.
Spawning new process is also known as forking
This terminology comes from the operating system community
Look at the picture upside-down and you got a fork!
Built-in functions
spawn
, spawn_link
, atom_to_list
Don’t need importing
Mostly located in module called erlang
(more here)
So far we considered synchronisation mechanisms based on shared variables
What about networked (distributed) architectures?
No shared memory
Message passing is the natural model for such scenarios
Words by Joe Armstrong (Programming Erlang - Software for a concurrent world)
The programming world went one way (toward shared state). The Erlang community went the other way. (Few other languages followed the “message passing concurrency” road. Others were Oz and Occam.)
We don’t have shared memory. I have my memory. You have yours. We have two brains, one each. They are not joined together. To change your memory, I send you a message: I talk, or I wave my arms. You listen, you see, and your memory changes;
It is relatively easy to adapt Erlang programs for distributed environments
One process sends a message
Another process awaits for a message
Dimensions for message passing approaches:
What form of synchronisation is required
What form of process naming is involved in message passing
Consider the behaviour of the sender of a message
Asynchronous send
Synchronous send
Rendezvous / Remote invocation
Erlang has asynchronous message passing
Ada has rendezvous
Java has libraries
Sockets – asynchronous message passing
Remote Method Invocation – it can be seen as synchronous/rendezvous message passing
How do sender and receiver refer to each other when message passing is used?
Direct symmetric
Direct asymmetric
Indirect
Naming a process is not always convenient
Naming an intermediary can be more flexible
A channel, service name, or a mailbox
Potentially many-to-many communication
Asynchronous send
Receive from a mailbox
Spawn a function evaluation in a separate thread
Function should be written using tail recursion
Interaction
Process A sends a token to a Process B
Process B just returns it back to Process A
-module(echo). -export([start/0]). echo() -> receive {From, Msg} -> From ! {self(), Msg}, echo(); stop -> true end. start() -> Pid = spawn(fun echo/0), % sending tokens to the server Token = 42, Pid ! {self(), Token}, io:format("Sent~w~n",[Token]), receive {Pid, Msg} -> io:format("Received ~w~n", [Msg]) end, % stop server Pid ! stop.
{Pid, Msg}
, where Pid
is bound)