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