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:

-module(learn_erlang).  % Module name matches the file name
-export([]).

% Put my functions here

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.

slope(X1, Y1, X2, Y2) ->
   Delta_X = X2 - X1,
   Delta_Y = Y2 - Y1,
   Delta_Y / Delta_X.

The function add exists with different arities which is why we write each one as a separate function separated by periods.

add(N1, N2) -> N1 + N2.
add(N1, N2, N3) -> N1 + N2 + N3.
add(N1, N2, N3, N4) -> N1 + N2 + N3 + N4.

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.

divide(_N1, 0) -> 0;
divide(N1, N2) -> N1 / N2.

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.

factorial(N) when N =< 1 -> 1;
factorial(N) -> N * factorial(N-1).

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

  1. Implement the add function to add two numbers.
  2. 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
  3. Implement the water function that will take a temperature in Fahrenheit and return “Frozen”, “Gas”, or “Liquid”. Implement this using three clauses that use the when guard.
  4. 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.
  5. Implement the sum function to add up the numbers from 0 to N (assume N is a positive integer) Use recursion instead of using any built-in functions. Recommend using body recursion.
  6. Modify the plot_test function to create lambda functions to pass to the plot function. The plot function will create {X,Y} coordinates where X goes from -5 to 5 and Y 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.

playing_with_lists() ->
   L1 = [1, 2, 3, 4],
   ok.

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.

playing_with_lists() ->
   L1 = [1, 2, 3, 4],
   L2 = [0|L1],
   [0, 1, 2, 3, 4] = L2,
   ok.

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

  1. Write a stack_push function that treats the list as a stack (LIFO - Last In First Out). The stack_push function will take two parameters, the Stack (ie, a list) and a value N. This function should push the value N to the front of the Stack.
  2. 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 [].
  3. 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 call quicksort 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.

\(\nonumber\) \(\nonumber\) Creative Commons License - CC - BY