Introduction
Erlang
Background
Sequential Erlang (basics)
Concurrent Erlang (basics)
Prominent applications
- Telecoms
- Switches (POTS, ATM, IP, ...)
- GPRS
- SMS applications
- Internet applications
- Whatsapp (article, another one)
- Klarna (online shopping)
- Formerly Facebook (chat)
- 3D modelling (Wings3D)
History lesson
- 1981 – Ericsson CSLab formed
- 1986 – Prolog “games”
- 1987 – Erlang name appears
- 1989 – Prototypes show 9–22 fold increase in design efficiency
- 1995 – AXE-N fails after 8 years
- 1998 – AXD301 delivered
- 1998 – Erlang banned; goes open source
- 2007 – Ericsson uses Erlang for new products
Essence
- Shared-nothing approach—isolated processes
- Each process runs a sequential program
- Sequential subset is functional
- Dynamically typed
- No shared memory
- Open Telecom Platform libraries
- Practically “proven” programming patterns
- Utilities
- A few important things done right
- A few less important things done wrong
Sequential aspects
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!)
Expressions
3 + 5
3
and5
are integers
true orelse false
true
andfalse
represent boolean valuesnot
,andalso
andorelse
are the standard (short-circuiting) boolean operators
Erlang is dynamically-typed, which means that expressions like
4 + true
are well-formed, and fail only at runtime
Interpreter
$ erl Erlang/OTP 18 [erts-7.0] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Eshell V7.0 (abort with ^G) 1> 3 + 5. 8 2>
Erlang interpreter allows for evaluating expressions
Each expression must be terminated with a
.
(fullstop)
Evaluation of expressions
3 + (2 * 4)
→ 3 + 8
→ 11
Evaluation continues until a value is reached
Integers and booleans are values
Operators applied to expressions are not values
Precedence
Task: You want to check how is the expression ? andalso ? orelse ?
decoded: is it (? andalso ?) orelse ?
or ? andalso (? orelse ?)
? In other words, is the precedence of andalso
higher than that of orelse
?
Design an experiment that will confirm one or the other.
Additional question: can the experiment really confirm it?
Functions
% This is a comment g(N) -> N + 2.
g
is the function nameN
is a varable—variables start with a capital letterN + 2
is the body of the function
Evaluation of functions
1> g(3) * g(1). 15 2>
g(3) * g(1)
→ (3 + 2) * g(1)
→ 5 * g(1)
→ 5 * (1 + 2)
→ 5 * 3
→ 15
The rules are the same as in Java
The difference is that we are not guaranteed that
g(3)
will be evaluated beforeg(1)
In practice the left-to-right evaluation order is used, but we shouldn't rely on it
Exception: short-circuiting
andalso
andorelse
operators do have a defined order of evaluation
Multiple clauses
h(2) -> 4; h(3) -> 6; h(N) -> N - 1.
Functions can have multiple clauses
Each clause has a pattern (here:
2
,3
andN
)When a function is called, its arguments are matched against the patterns, starting with the first clause
The first clause whose pattern matches the argument will be taken
For example
h(3)
will return6
, as the second caluse will be chosen
Repeated variables
myequal(N, N) -> true; myequal(N, M) -> false.
- Repeated variable must match the same value in all occurrences
Guards
h2(1) -> 1; h2(N) when N < 5 -> 3; h2(N) -> N + 1.
A guard is a boolean expression associated with a clause
The clause is taken only when the guard evaluates to
true
Only a limited subset of expressions is allowed (no function calls, except for some builtin functions)
Recursion
sum_m_n(M, N) when M > N -> 0; sum_m_n(M, N) -> M + sum_m_n(M+1, N).
A function is recursive if it calls itself (directly or indirectly)
Recursion is the way to implement loops in Erlang
sum_m_n(M, N)
takes two integers and computes the sum of numbers fromM
toN
inclusive
sum_m_n(2, 4)
→
2 + sum_m_n(3, 4)
→
2 + (3 + sum_m_n(4, 4))
→
2 + (3 + (4 + sum_m_n(5, 4)))
→
2 + (3 + (4 + 0))
→
2 + (3 + 4)
→
2 + 7
→
9
Tail-recursion
% The same function reimplemented using tail-recursion sum_m_n_i(M, N, Acc) when M > N -> Acc; sum_m_n_i(M, N, Acc) -> sum_m_n_i(M+1, N, Acc + M). % We define a convenience function sum_m_n_i(M, N) -> sum_m_n_i(M, N, 0).
A recursive call is a tail call if it is the last thing to be evaluated in the given body of the function
Tail-recursive functions can be used to implement accumulating loops
Sometimes tail-recursion is preferred for efficiency
Functions with the same name and different numbers of arguments can be defined (they are treated as different functions)
sum_m_n_i(2, 4)
→
sum_m_n_i(2, 4, 0)
→
sum_m_n_i(3, 4, 0 + 2)
→
sum_m_n_i(3, 4, 2)
→
sum_m_n_i(4, 4, 2 + 3)
→
sum_m_n_i(4, 4, 5)
→
sum_m_n_i(5, 4, 5 + 4)
→
sum_m_n_i(5, 4, 9)
→
9
- In this case we exploit the fact that it does not matter in which direction we add numbers
Multiple statements, variables
f(X) -> X * 2. g(N) -> Y = f(N-1), Y + 2.
Comma separates multiple statements executed in a sequence
A statement is either an expression or an assignment
An assignment
X = E
has a pattern on its right-hand side and an expression on the left-hand sideIf the result of evaluating the expression matches the pattern, then the variables of the pattern are assigned values
Otherwise, the assignment fails (runtime error)
Variables that are bound (are assigned values) are treated as ordinary values in a pattern
g(5)
→
Y = f(5-1),
Y + 2
→
Y = f(4),
Y + 2
→
Y = 4 * 2,
Y + 2
→
8 + 2
→
10
Data types
Numbers
- Integers (arbitrarily big)
- Floats
Atoms
start_with_a_lower_case_letter ’Anything_inside_quotes\n\09’
Tuples
{} {atom, another_atom, ‘PPxT’} {atom, {tup, 2}, {{tup}, element}}
- Lists
[] [1, true] [a, b, c] = [a | [b, c]] = [a | [b | [c]]] = [a | [b | [c|[]]]]
- Lists have two primitive constructors
[]
: empty list[X | Y]
: non-empty list, consisting of the first element (head) and the rest of the list (tail)
- Lists also have a convenience notation
[A, B, C, D]
- Another notation is also available:
[A, B | C]
, which is the same as[A | [B | C]]
- Lists have two primitive constructors
- Strings
"This is a string"
- Strings are just list of numbers (ASCII codes) having a convenience notation
- Lists are printed as strings when all numbers that they contain correspond to printable characters
- Both inconvenient and slow:(
Modeling data
- Complex data types are typically created using tuples decorated with atoms
{shape, {triangle, 2, 2, 3}, blue} {shape, {square, 3}, red} {shape, {rectangle, 3, 4}, white}
- Algebraic data types can be represented in this way
data Tree a = Leaf a | Node (Tree a) (Tree a)
edoc style “types”
- Atoms to indicate which constructor is used at top-level
- Tuples to collect the arguments of the constructor
Example:
Node (Node (Leaf 3) (Leaf 4)) (Leaf 5)
becomes{node, {node, {leaf,3}, {leaf, 4}}, {leaf, 5}}
Pattern matching
Function
area
to compute the area of different geometric figures.area({square, Side}) -> Side * Side; area({circle, Radius}) -> Radius*Radius*math:pi().
Patterns:
{square, Side}
and{circle, Radio}
{square, Side}
matches{square, 4}
and binds4
to variableSide
{circle, Radius}
matches{circle, 1}
and binds1
to variableRadius
More pattern matching
{B, C, D} = {10, foo, bar}
- Succeeds: binds
B
to10
,C
tofoo
andD
tobar
- Succeeds: binds
{A, A, B} = {abc, abc, foo}
- Succeeds: binds
A
toabc
,B
tofoo
- Succeeds: binds
{A, A, B} = {abc, def, 123}
- Fails
[A,B,C,D] = [1,2,3]
- Fails
Even more pattern matching
[H|T]= [1,2,3,4]
- Succeeds: binds
H
to1
,T
to[2,3,4]
- Succeeds: binds
[H|T] = [abc]
- Succeeds: binds
H
toabc
,T
to[]
- Succeeds: binds
[H|T] = []
- Fails
{A, _ , [ B | _ ] , {B} } = {abc,23,[22,x],{22}}
- Succeeds: binds
A
toabc
,B
to22
- Succeeds: binds
Consuming lists
Computing the length of a list
len([X|XS]) -> 1 + len(XS); len([]) -> 0.
Note: [a, b, c]
is the same as [a | [b, c]]
len([a, b, c])
→
1 + len([b, c])
→
1 + (1 + len([c]))
→
1 + (1 + (1 + len([])))
→
1 + (1 + (1 + 0))
→
1 + (1 + 1)
→
1 + 2
→
3
The function is defined recursively (inductively)
- Base case: empty list (
[]
) - Recursive case: a list with at least one element (
[X | XS]
)
- Base case: empty list (
Tail recursive version
len_i([], Acc) -> Acc; len_i([X|XS], Acc) -> len_i(XS, Acc+1). len_i(XS) -> len_i(XS, 0).
len_i([a, b, c])
→
len_i([a, b, c], 0)
→
len_i([b, c], 0 + 1)
→
len_i([b, c], 1)
→
len_i([c], 1 + 1)
→
len_i([c], 2)
→
len_i([], 2 + 1)
→
len_i([], 3)
→
3
List examples
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]
Adding elements of a list
Simplest case (base case)
sum([]) == 0
The inductive case
- Focus on an example
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 tosum
. Let us associate the sum as follows.sum([X1,X2,X3,X4]) == X1 + (X2 + X3 + X4)
We know that
X2 + X3 + X4
is equal tosum([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])
- We generalize the last equation for the case when the
list has a head element
X
and the rest of the list isXS
sum([X | XS]) == X + sum(XS)
- We can now define the
sum
function as follows.
sum([X|XS]) -> X + sum(XS); sum([]) -> 0.
Why is the base case as second clause?
- We generalize the last equation for the case when the
list has a head element
- Focus on an example
More examples
Computing the length of a list
len([X|XS]) -> 1 + len(XS) ; len([]) -> 0.
- Observe that
X
is not used on the right-hand side. It can be replaced by_
len([_|XS]) -> 1 + len(XS) ; len([]) -> 0.
- Observe that
Append elements to the end of a list
append([X|XS],YS) -> [X | append(XS,YS) ] ; append([], YS) -> YS.
Side effects
Each statement may have side effects
f() -> io:format("line 1~n"), io:format("line 2~n"), io:format("line 3~n").
Case statements
Function
area()
can be rewritten using a case statementarea({square, Side}) -> Side * Side; area({circle, Radius}) -> Radius*Radius*math:pi().
area(X) -> case X of {square, Side} -> Side * Side; {circle, Radius} -> Radius*Radius*math:pi() end.
area({square, 3})
→
case {square, 3} of
{square, Side} -> Side * Side;
{circle, Radius} -> Radius*Radius*math:pi()
end
→
3 * 3
→
9
- Case statements can also have guards
If statements
f(X) -> if X > 2 -> X + 2; true -> X - 2 end.
- Limited version of a case statement, where only guards are used
Modules
Basic compilation unit is a module
- Module name = file name (.erl)
Modules contain function definitions
- Like
factorial
andarea
(see above) - Some functions can be exported to be usable from outside of the module
- Like
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
Data types (Records)
- Definition
-record(person, {name = “”, phone = [], address}).
- Creation
X = #person{name = “Joe”, phone = [1,1,2], address= “here”}
- Accessing record fields
X#person.name X#person{phone = [0,3,1,1,2,3,4]}
A note on the use of records and functions
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