Chapter 1 - Erlang Primer
In this course, we will be using the Erlang programming language. The material that we will cover could be implemented in many different languages. Erlang will allow us to use less code to explore the design patterns and data structures. If you haven’t learned Erlang before, the first two chapters of this book will help you to quickly learn the language. As the course progresses, you will find that the implementation of future problem sets will improve your Erlang skills.
1.1 Basic Syntax
This tutorial is not meant to provide complete coverage of the Erlang language. You are encouraged to use the Erlang references you bookmarked as needed throughout the semester.
When writing code in Erlang, you will put your code into a .erl
file. Each Erlang file will include a -module
and an -export
tag. If I create a file called learn_erlang.erl
, then I would create the following file:
The -export
tag is used to list all functions that can be run externally. Functions are written in the format of name(Parameters) -> expressions.
Note that parameters and variables always start with an uppercase letter. Functions and atoms (which are labels without values) are always lowercase. In the code below, we have two functions including the classic “Hello World” function and a function to calculate an average. The -export
tag shows the number of parameters (or the arity) of the function. Notice that each function ends with a period.
-module(learn_erlang).
-export([hello/0, average/2]).
hello() -> io:format("Hello World!~n").
average(Number1, Number2) -> (Number1+Number2) / 2.
To run the functions, execute the erl
shell command from a terminal. The c
command in the erl
shell will compile a module. To run a function, type module_name:function_name(parameters).
Notice the need to add the period just like in your code file.
Eshell V12.0 (abort with ^G)
1> c(learn_erlang).
{ok,learn_erlang}
2> learn_erlang:average(10,20).
15
In the slope
function, multiple expressions are used for a slightly more complicated function. Commas are used to separate expressions in the function with a final period at the end. The last expression represents the result of the function.
The function add
exists with different arities which is why we write each one as a separate function separated by periods.
In the function divide
, we have only arity 2 but we have two different scenarios or clauses. Each clause is separated by semicolons and is evaluated in order until a match is found. If we divide by 0, then the first clause will run. If we divide by non-zero, then the second clause will run. In the divide
function, also note that the first clause did not need to use N1
in the expression. Unused parameters in Erlang are prefixed with an underscore. You can replace the _N1
with just _
but we will often include the name for readability.
The when
keyword is called a guard and can be used with clauses. Basic boolean logic can be done with a guard. For more complicated logic, a case
statement can be used which will be seen later.
letter_grade(Grade) when Grade >= 90 -> "A";
letter_grade(Grade) when Grade >= 80 -> "B";
letter_grade(Grade) when Grade >= 70 -> "C";
letter_grade(Grade) when Grade >= 60 -> "D";
letter_grade(_Grade) -> "F".
With the understanding of clauses, we can implement recursive solutions. In this example, the base case for factorial
is represented by the first clause (notice the =<
syntax) and recursion is used in the second clause. The first clauses could have been rewritten without a guard if we didn’t worry about negative number as: factorial(0) -> 1;
We call this method of recursion “body recursion” because there are operations (multiply by N
) outside of the recursive call.
We can rewrite the same factorial
function in what is called “tail recursion” format. In this case, there is no operation outside of the recursive call. This requires that we have the Result
parameter to keep track of the ever-growing value. To support a Result
parameter, we provide a 1-arity version of the factorial
function for the user to call which initializes the product to 1. When the base case is reached (in this case N is equal to 0), then we can just return the result we have been recursively growing.
factorial(N) -> factorial(N, 1).
factorial(0, Result) -> Result;
factorial(N, Result) -> factorial(N-1, N*Result).
In Erlang, both recursion approaches result in the same performance. The readability of the specific problem you are solving should determine the method you use. One might argue that in this case, that the “body recursion” method was easier to read.
In the encrypt
code below, the second parameter Cipher_Function
is a function. Common in functional programming, functions are passed and used liked they were any parameter like integers and strings. The fun (parameters) -> expression end
syntax is used to define an anonymous function (often called a lambda function). The encrypt
function expects to receive a function that takes only one parameter. The fun math:log10/1
is an example of using an existing 1-arity function from the Erlang libraries.
encrypt(Value, Cipher_Function) -> Cipher_Function(Value).
test_encrypt() ->
Cipher1 = fun (X) -> X + 1 end,
Cipher2 = fun (X) -> (2 * X) - 3 end,
11 = encrypt(10, Cipher1),
17 = encrypt(10, Cipher2),
3.0 = encrypt(1000, fun math:log10/1),
ok.
In the test_encrypt
function, note that Cipher1
and Cipher2
variables were created. We could not change the value of Cipher1
because all variables in Erlang are immutable.
Problem Set 1
Starting Code: prove01_1/src/prove01_1.erl
Write code as described in the instructions below and in the comments found in the starting code. You may need to comment out some test functions or modify the -export
tag to debug your code. To run the tests in the code, run the following from the prove01 folder: rebar3 eunit
- Implement the
add
function to add two numbers. - Implement the
multiply
function three different ways each with a different number of parameters (i.e. different arity):- Multiply one number (not very exciting)
- Multiply two numbers
- Multiply three numbers
- Implement the
water
function that will take a temperature in Fahrenheit and return “Frozen”, “Gas”, or “Liquid”. Implement this using three clauses that use thewhen
guard. - Implement the
fib
function to calculate the nth Fibonacci number. Assume the 1st and 2nd number is 1. You will need to use recursion. Recommend using body recursion. - Implement the
sum
function to add up the numbers from 0 toN
(assumeN
is a positive integer) Use recursion instead of using any built-in functions. Recommend using body recursion. - Modify the
plot_test
function to create lambda functions to pass to theplot
function. Theplot
function will create{X,Y}
coordinates whereX
goes from -5 to 5 andY
is calculated from your lambda function. The curly braces represent a tuple in Erlang. Create lambda functions for the following scenarios:- Function that squares the number
- Function that subtracts one from the number
- Pass the
abs/1
built-in function directly - Function that divides the number by three and then uses the
math:floor/1
built-in function to remove the decimal.
1.2 Lists in Erlang
Lists are built-in to Erlang at the syntax level. During the course, we will write our own functions to work with lists and we will even re-create our own list data structure. To support those activities, we need to understand how to use the built-in list first. Lists can be created using square brackets.
We can use a vertical bar to access the first element of the list. The syntax [0|L1]
prepends the 0
in front of the L1
list. The [0, 1, 2, 3, 4] = L2
will attempt to pattern match L2
with the list on the left-hand side. If this doesn’t match, our “test” will fail.
Pattern matching can reuse the [First|Rest]
format. In the example above, we can extract the first or the first several values. We can match multiple values at the front of the list using commas. Note the use of the _
which indicates we are ignoring the “rest” of the list.
playing_with_lists() ->
L1 = [1, 2, 3, 4],
[A | _] = L1,
1 = A,
L2 = [2, 4, 6, 8],
[B, C | _] = L2,
2 = B,
4 = C,
ok.
In the example below, When the list L1
is passed to the display_first
function, the [First|_Rest]
syntax will split the list up into the first value and the remainder of the list (the latter of which is not used in the function.
display_first([First|_Rest]) ->
io:format("~p~n",[First]).
playing_with_lists() ->
L1 = [1, 2, 3, 4],
display_first(L1),
ok.
In this example, a special clause is added for the empty list. We use []
to represent an empty list. If we wanted to represent a list of one item, we could use [One]
. If we wanted to represent a list of more than one item, we should use [First|Rest]
.
display_first([]) ->
io:format("EMPTY~n");
display_first([First|_Rest]) ->
io:format("~p~n",[First]).
playing_with_lists() ->
L1 = [1, 2, 3, 4],
display_first(L1),
display_first([]),
ok.
In this code, we need to use the entire list to display every value. Recursion is used on the Rest
of the list. The base case is the empty list.
display_all([]) ->
io:format("end of list~n");
display_all([First|Rest]) ->
io:format("~p ",[First]),
display_all(Rest).
playing_with_lists() ->
L1 = [1, 2, 3, 4],
display_all(L1),
display_all([]),
ok.
There are multiple ways to create lists. The lists:seq
function will create numbers in a list from the range specified. The other method is the list comprehension. The syntax for the list comprehension is: [expression || generator, filter]
The generator
is written with a <-
and provides a source list. The expression
uses the generated number to add to the list. The optional filter
is used to determine which generated values should be used to generate the list. In the last example, the rem
operator (often called modulo) is used to include only even numbers before tripling them.
playing_with_lists() ->
L1 = lists:seq(1,10),
[1,2,3,4,5,6,7,8,9,10] = L1,
L2 = [N || N <- lists:seq(1,10)],
[1,2,3,4,5,6,7,8,9,10] = L2,
L3 = [N*3 || N <- lists:seq(1,10)],
[3,6,9,12,15,18,21,24,27,30] = L3,
L4 = [N*3 || N <- lists:seq(1,10), N rem 2 == 0],
[6,12,18,24,30] = L4,
ok.
Problem Set 2
Starting Code: prove01_2/src/prove01_2.erl
- Write a
stack_push
function that treats the list as a stack (LIFO - Last In First Out). Thestack_push
function will take two parameters, theStack
(ie, a list) and a valueN
. This function should push the valueN
to the front of theStack
. - Write a
stack_pop
function that returns a new stack with the first item removed. If the stack is empty, then return the empty list[]
. - Finish the
quicksort
function provided to you in the starting code. The algorithm for quicksort is to first pick a position in the list (in our case we will pick the first number) and call it our pivot. We will then recursively callquicksort
on all of the numbers less than the pivot and then call quicksort on all the numbers greater than or equal to the pivot. The correct sorted list is then the concatenation (++
) of the following 3 lists:[sorted list of numbers less than the pivot] ++ [the one pivot] ++ [sorted list of numbers greater than or equal to the pivot]
. The code is setup already but you need to write list comprehensions for creating:- A sub-list containing all numbers in the list less than the pivot (first number in the list) which will be passed to the
quicksort
function. - A sub-list containing all numbers in the list greater than or equal to the pivot (first number in the list) which will be passed to the
quicksort
function.
- A sub-list containing all numbers in the list less than the pivot (first number in the list) which will be passed to the