Erlang Course Notes

Author: Dave Kuhlman
Contact: dkuhlman (at) davekuhlman (dot) org
Address:
http://www.davekuhlman.org
Revision: 1.0a
Date: July 25, 2016
Copyright:Copyright (c) 2016 Dave Kuhlman. All Rights Reserved. This software is subject to the provisions of the MIT License http://www.opensource.org/licenses/mit-license.php.
Abstract:This document provides notes and how-to information on programming in Erlang. It can hopefully serve as a cheat sheet on Erlang programming.

Contents


1   Introductions Etc

This is a set of usage notes on Erlang. As with any programming language, some of the things that you need to know are hard to find or slow to search for and lookup. This document will hopefully save you a bit of time with that. This document can also be viewed as an introductory how-to for Erlang programming.

1.1   Help and docs

The Erlang standard documentation is here:

You might also consider downloading the Erlang document and unrolling it on your local machine, which will enable you to jump around among various links more quickly. You can find that here: http://www.erlang.org/downloads.

There is a FAQ and more docs here: http://www.erlang.org/docs.

1.2   The erl interactive shell

Start it as follows:

$ erl
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V8.0  (abort with ^G)
1>

On Linux, you might want to install and use rlwrap. It saves history across sessions. I use the following bash alias to start the erl shell:

alias erlrl='rlwrap -a -c -r erl'

Or, if you don't want the alias, just start the shell as follows:

$ rlwrap -a -c -r erl

For more information about the Erlang shell (emulator), see: http://erlang.org/doc/man/erl.html.

On MS Windows, you will want to consider using werl instead of erl. werl has some GUI (graphical user interface) features that as not provided by plain erl.

1.3   Compiling to .beam files

In the interactive shell use:

7> c(test01).
{ok,test01}

You can also pass in a compiler option. For example, to compile a file for use in the Erlang debugger, use something like this:

8> c(test01, [debug_info]).
{ok,test01}

At the command line, use erlc (http://erlang.org/doc/man/erlc.html):

$ erlc test01.erl

And, with a compiler option:

$ erlc +debug_info test01.erl

1.4   Debugging and the Erlang debugger

Erlang has a very nice visual/graphical debugger. And, it enables you to step through multiple, concurrent, Erlang processes.

In the Erlang shell, use the internal shell command ih() to get more help with the debugger. Example:

21> ih().
iv()         -- print the current version of the interpreter
im()         -- pop up a monitor window
ii(Mod)      -- interpret Mod(s) (or AbsMod(s))
ii(Mod,Op)   -- interpret Mod(s) (or AbsMod(s))
    o
    o
    o

To use the debugger:

  1. Compile your modules with the debug_info option. In the Erlang shell, use c(my_module, [debug_info]).. From the command line, use erlc +debug_info my_module.

  2. Start the Erlang shell:

    $ erl
    
  3. Start the debugger:

    2> im().
    <0.36.0>
    
  4. Load your module into the debugger:

    3> ii(ca02).
    {module,ca02}
    

    Alternatively, you could load your module by using the Module menu in the debugger Monitor window.

  5. Set a breakpoint. You can do this in the Monitor window by selecting the Module menu, then your module, and then View. Or, alternatively, double click the module name in the top left pane of the Monitor window. A debugger view window will appear.

    Then, double click on the line where you want to set a breakpoint.

  6. Run your test. Now, back in the Erlang shell, call the function or functions that run your test. For example:

    4> {ok, G} = ca02:initialize("test04.csv").
    
  7. Step through, inspect, and debug your code. If the debugger arrived at one of your breakpoints, then in the debugger Monitor window, that process should be listed with a status of "break". Double click on that line, and the debugger will present a window where you can begin stepping through and inspecting the behavior of that process.

2   Built-in functions

The documentation on BIFs is here: http://erlang.org/doc/apps/erts/index.html.

3   Expressions and operators

Information on various kinds of expressions can be found here: http://erlang.org/doc/reference_manual/expressions.html. Information on operators can be found in that section. Look in sub-sections titled "Term comparisons", "Arithmetic expressions", and "Boolean expressions".

4   Built-in data types

Additional information on Erlang data types can be found here: http://erlang.org/doc/reference_manual/data_types.html.

4.1   Atoms

Atoms are self-defining names. They either begin with a lower case letter, or are surrounded by single quotes. Examples:

5> A = abc.
abc
6> B = 'aa.bb.cc'.
'aa.bb.cc'
7> B.
'aa.bb.cc'
8> A.
abc

You can use the is_atom BIF (built-in function) to test for an atom.

4.2   Tuples

A tuple is defined by zero or more terms inside curly brackets. Examples:

9> {}.
{}
10> {aa}.
{aa}
11> {aa, bb, cc}.
{aa,bb,cc}
12>
12> C = {nil, nil, [11, 22, 33], nil}.
{nil,nil,[11,22,33],nil}

You can use the is_tuple BIF (built-in function) to test for an tuple.

4.3   Lists

A list is defined by zero or more terms inside square brackets. Examples:

13> f().
ok
14> A = [100, 200, 300].
[100,200,300]
15> B = [aaa, A, bbb].
[aaa,[100,200,300],bbb]
16>
16> C = ["abc", "def", "ghi"].
["abc","def","ghi"]
17>
17> D = "bcdef".
"bcdef"
18> D1 = ["a" | D].
["a",98,99,100,101,102]
19> lists:flatten(D1).
"abcdef"

Notes:

  • The f() shell function causes the shell to forget all our variable bindings in the shell, so that we can bind those variables again. Use help() to get a list of additional shell internal commands.
  • Remember that a string is a list of characters. So, if we "flatten" a list that contains characters and a list of characters, we get a list of characters, which is a string.
  • We can use functions from the lists module on strings, because they are lists. See: http://erlang.org/doc/man/lists.html.
  • Lists can, of course, be nested inside of lists, and to any depth.

You can use the is_list BIF (built-in function) to test for an list.

Use the BIFs hd and tl to access the head and tail of a list. Examples:

10> C.
[11,22,33]
11> hd(C).
11
12> tl(C).
[22,33]

Use the [X | Y] construction to build a list by appending an item to the head of a list or (2) to deconstruct a list into its head and tail. Examples:

1> A = 11.
11
2> B = [22, 33].
[22,33]
3> C = [A | B].
[11,22,33]
4> C.
[11,22,33]
5>
5> f().
ok
6> C = [11, 22, 33].
[11,22,33]
7> [A | B] = C.
[11,22,33]
8> A.
11
9> B.
[22,33]

This is especially useful when writing recursive functions on lists. Example:

list_test01([], Accum) ->
    Accum;
list_test01([Head | Tail], Accum) ->
    Accum1 = Accum + Head,
    list_test01(Tail, Accum1).

When we run the above we might see:

2> test01:list_test01([11, 22, 33, 44], 0).
110

4.4   Strings

Strings in Erlang are just lists of characters. So, many of the functions in the lists module are helpful with strings, too. Some examples:

37> f().
ok
38>
38> A = "abcdef".
"abcdef"
39> length(A).
6
40> lists:reverse(A).
"fedcba"
41> io:fwrite("Variable A: ~p~n", [A]).
Variable A: "abcdef"
ok
42>
42> io:fwrite("Variable A: ~s~n", [A]).
Variable A: abcdef
ok

Notes:

  • The function to determine the length of a list or a string is length/1, and it's a BIF (built in function), not a function in the lists module.
  • Strings are formatted differently depending on whether we use the ~p or the ~s format specifier.

To iterate over the characters in a string, use a standard Erlang recursive function, just like you would with a list:

-module(test01).

-export([double_chars/2]).

double_chars([], Accum) ->
        lists:reverse(Accum);
double_chars([Char | MoreChars], Accum) ->
        Accum1 = [Char | Accum],
        Accum2 = [Char | Accum1],
        double_chars(MoreChars, Accum2).

When we call it, we see:

1> c(test01).
{ok,test01}
2> A = "abcdef".
"abcdef"
3> test01:double_chars(A, []).
"aabbccddeeff"

Notes:

  • The first clause of double_chars/2 is our stop or terminating clause. It checks for the terminating condition (and empty list, in this case), then reverses and returns the list of characters (i.e. the string.
  • The second clause strips one character off the input list, appends that character twice to the accumulator (a list), and then calls itself (the recursive part) with the remainder of the characters and this accumulated list.

4.4.1   Formatting and printing strings

To format and print a string, use io:format or io:fwrite: http://erlang.org/doc/man/io.html. They do the same thing.

To format a string (but not print it), use iolib:format: http://erlang.org/doc/man/io_lib.html.

Examples:

44> A = "red".
"red"
45> B = "yellow".
"yellow"
46>
46> io:format("The ~s apple and the ~s banana", [A, B]).
The red apple and the yellow bananaok
47>
47> io:format("The ~s apple and the ~s banana~n", [A, B]).
The red apple and the yellow banana
ok

There is no test for the "string data type", since strings in Erlang are lists, though you could use the is_list BIF to at least determine whether it is a list.

4.5   Maps

A map associates keys with values.

Create a map -- Example:

17> A = #{aa=>11, bb=>22, cc=>33, 12=>"abc", "doc"=>"bark"}.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}
18> A.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}

Update a map -- Example:

20> A.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}
21>
21> A1 = A#{aa=>111}.
#{12 => "abc",aa => 111,bb => 22,cc => 33,"doc" => "bark"}
22> A1.
#{12 => "abc",aa => 111,bb => 22,cc => 33,"doc" => "bark"}

Update an existing key in a map -- If you want to update an existing key in a map, and you want an error thrown if the key does not exist, use the following syntax:

23> A.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}
24> B = A#{bb := 222}.
#{12 => "abc",aa => 11,bb => 222,cc => 33,"doc" => "bark"}
25>
25> B1 = A#{xx := 222}.
** exception error: {badkey,xx}
     in function  maps:update/3
        called as maps:update(xx,222,#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"})
     in call from erl_eval:'-expr/5-fun-0-'/2 (erl_eval.erl, line 255)
     in call from lists:foldl/3 (lists.erl, line 1263)

Pattern matching on maps -- We can bind variables to the values of keys in a map. Examples:

37> A.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}
38> #{cc := C, bb := B} = A.
#{12 => "abc",aa => 11,bb => 22,cc => 33,"doc" => "bark"}
39> B.
22
40> C.
33

There are functions for working with maps in the maps module in the standard library: http://erlang.org/doc/man/maps.html.

For more information on maps, see: http://erlang.org/doc/reference_manual/expressions.html#map_expressions.

4.6   Funs

Define a fun -- We define a fun by using the fun ... end keywords. Example:

41> F1 = fun (X, Y) -> io:format("X: ~p  Y: ~p~n", [X, Y]), ok end.
#Fun<erl_eval.12.52032458>
42>
42> F1(11, 22).
X: 11  Y: 22
ok

Multiple clauses -- A fun can have multiple clauses. Pattern matching on the heads of the clauses is performed in the same way as for functions. Example:

fun_test01(A, B) ->
    F = fun (key1, X) -> io:fwrite("key1 -- X: ~p~n", [X]), gotkey1;
            (key2, X) -> io:fwrite("key2 -- X: ~p~n", [X]), gotkey2
        end,
    Reply = F(A, B),
    Reply.

When we call the above, we'd see:

44> c(test01).
{ok,test01}
45> test01:fun_test01(key1, 45).
key1 -- X: 45
gotkey1
46> test01:fun_test01(key2, 55).
key2 -- X: 55
gotkey2
47>
47>
47> test01:fun_test01(key3, 55).
** exception error: no function clause matching
                    test01:'-fun_test01/2-fun-0-'(key3,55) (test01.erl, line 166)
48>

Named fun -- A fun definition can provide a name. That enables us to call the fun recursively, that is, from within the fun itself. Example:

fun_test02(Data) ->
    F = fun Travel ([], Accum) -> Accum;
            Travel ([Item | Moredata], Accum) ->
                Travel(Moredata, Accum + Item)
        end,
    Result = F(Data, 0),
    Result.

When we call the above function, we might see:

1> c(test01).
{ok,test01}
2> test01:fun_test02([44, 33, 22, 11]).
110

Notes:

  • In a named fun, the name must be a variable, i.e. start with an upper case letter.

Define a fun by name and arity -- We can define a fun by using an existing function and it's arity, for example Name/Arity or Module:Name/Arity. This gives us an easy way to produce a fun from an exiting function. Example:

fun_test03_helper(X, Y) ->
    Sum = X + Y,
    io:fwrite("X: ~p  Y: ~p  Sum: ~p~n", [X, Y, Sum]),
    Sum.

fun_test03(V1, V2) ->
    F = fun fun_test03_helper/2,
    Result = F(V1, V2),
    Result.

When we call fun_test03/2, we might see:

fun_test03_helper(X, Y) ->
    Sum = X + Y,
    io:fwrite("X: ~p  Y: ~p  Sum: ~p~n", [X, Y, Sum]),
    Sum.

fun_test03(V1, V2) ->
    F = fun fun_test03_helper/2,
    Result = F(V1, V2),
    Result.

For more information on fun, see: http://erlang.org/doc/reference_manual/expressions.html#funs.

5   Functions

A function definition can have multiple clauses separated by semi-colons. The head of each clause is composed of the function name and a list of arguments. Each heads of the clauses in a single function definition must have the same arity.

For example, here is a function that simulates the map function from the lists module stdlib. This example defines two functions: map/2 and map/3, one with arity 1 and the other with arity 3. The function map/3 has two clauses: one checks for an empty list and the other processes one item from the list:

map(Func, Items) ->
    map(Func, Items, []).

map(_, [], Accum) ->
    lists:reverse(Accum);
map(Func, [Item | MoreItems], Accum) ->
    Result = Func(Item),
    map(Func, MoreItems, [Result | Accum]).

Notes:

6   Records

Records are just a wrapper around tuples.

Define a record -- Use the -record(name, [field, ...]). instructive to define a record:

Examples:

-record(celldata, {
          state,            % State = 0 | 1
          nextstate,        % State = 0 | 1
          neighbors,        % [Pid, ...]
          neighborstates    % [{Pid, State}, ...]
         }).

Create an instance of a record -- This example shows how to create an instance of a record, given its definition:

14> rd(kitten, {name, color}).
kitten
15> Kitten1 = #kitten{name=taffy, color=carmel}.
#kitten{name = taffy,color = carmel}
16>
16> rd(puppy, {name, size=medium}).
puppy
17> Puppy1 = #puppy{name=frisky}.
#puppy{name = frisky,size = medium}
18>
18> rl().
-record(kitten,{name,color}).
-record(puppy,{name,size = medium}).
ok

Notes:

Access (get) the value of a field -- We can extract the value of a field either (1) using a "dot" notation or (2) using pattern matching. Examples:

rec_test4() ->
    R1 = #record1{field1=111, field2=[aa,bb], field3=333},
    io:fwrite("R1: ~p~n", [R1]),
    io:fwrite("R1.field2: ~p~n", [R1#record1.field2]),
    #record1{field2=Fld2, field3=Fld3} = R1,
    io:fwrite("Fld2: ~p  Fld2: ~p~n", [Fld2, Fld3]),
    ok.

Replace the value of a field -- Reuse the old record, but replace the values of selected fields to create a new record. Examples:

rec_test1() ->
    R1 = #record1{},
    R2 = R1#record1{field1=100},
    io:fwrite("R1: ~p~n", [R1]),
    io:fwrite("R2: ~p~n", [R2]),
    ok.

rec_test2() ->
    R1 = #record1{field1=11, field3=33},
    R2 = R1#record1{field1=25, field3=50},
    io:fwrite("R1: ~p~n", [R1]),
    io:fwrite("R2: ~p~n", [R2]),
    ok.

rec_test3() ->
    R1 = #record1{field1=11, field2=[aa,bb,cc], field3=33},
    R2 = R1#record1{field2=[zz | R1#record1.field2]},
    io:fwrite("R1: ~p~n", [R1]),
    io:fwrite("R2: ~p~n", [R2]),
    ok.

Notes:

7   Statements

7.1   case statement

A case statement is composed of multiple clauses of the form: head --> body. The first clause whose head matches the target value is then evaluated. The value of the case statement is the value of the last expression in the body of the evaluated clause.

Example:

case_test(Arg) ->
    Reply = case Arg of
        {cmd1, Command, Arg1} ->
            io:fwrite("cmd1 -- Command: ~p  Arg1: \"~p\"~n", [Command, Arg1]),
            reply1;
        {cmd2, Command, Arg1} ->
            io:fwrite("cmd2 -- Command: ~p  Arg1: \"~p\"~n", [Command, Arg1]),
            reply2;
        Stuff ->
            io:fwrite("catch-all -- Stuff: ~p~n", [Stuff]),
            misc
    end,
    Reply.

When we call the above, we might see:

2> test01:case_test(abc).
catch-all -- Stuff: abc
misc
3> test01:case_test([aaa, bbb, ccc]).
catch-all -- Stuff: [aaa,bbb,ccc]
misc
4> test01:case_test({cmd1, push, [11, 22, 33, 44]}).
cmd1 -- Command: push  Arg1: "[11,22,33,44]"
reply1

Notes:

  • We can use tuples (or lists) to pass in multiple values.
  • The matching process is very similar to that done on the head of a function.
  • As in the matching process performed on the heads of clauses of functions, it is possible to bind values to variables while matching the head of a clause.
  • In our sample code, the case statement returns one of reply1, reply2, or misc, and binds that value to the variable Reply.

7.2   if statement

The if statement is similar to the case statement in some ways, but uses boolean expressions rather than pattern matching. The boolean expressions that form the heads of clauses in the if statement are very much like guards expressions.

Example:

if_test(Arg) ->
    Reply =
    if
        Arg >= 4 ->
            io:fwrite("Arg ~p greater than or equal to 4~n", [Arg]),
            {greater, Arg};
        (Arg =< 3) and (Arg >= 1) ->
            io:fwrite("Arg ~p between 1 and 3~n", [Arg]),
            {less, Arg};
        Arg == 0 ->
            io:fwrite("Arg ~p is zero~n", [Arg]),
            {equal, Arg};
        Arg == -1; Arg == -2 ->
            io:fwrite("Arg ~p is -1 or -2~n", [Arg]),
            {equal, Arg};
        true ->
            io:fwrite("Unexplected value -- Arg: ~p~n", [Arg]),
            {error, "unexpected"}
    end,
    Reply.

And, when we call the above, we see:

18> test01:if_test(0).
Arg 0 is zero
{equal,0}
19> test01:if_test(6).
Arg 6 greater than or equal to 4
{greater,6}
20> test01:if_test(2).
Arg 2 between 1 and 3
{less,2}
21> test01:if_test(-8).
Unexplected value -- Arg: -8
{error,"unexpected"}

Notes:

  • We can use any boolean expressions, even complex ones for the heads of our clauses. However, these expressions must satisfy the rules for guard expressions. See here for a description of guards: http://erlang.org/doc/reference_manual/expressions.html (and look for "Guard Sequences").
  • Notice that complex expressions (for example, those that contain boolean operators and and or) often require parentheses. That's because and and or have higher precedence/priority (bind more tightly) than operators such as ==, >=, etc. See: http://erlang.org/doc/reference_manual/expressions.html (and look for "Operator Precedence").
  • The head of a clause may contain multiple expressions separated by semicolons, in which case that clause is selected if any of those expressions are true. For example, notice the head Arg == -1; Arg == -2 -> in our sample code, above.
  • We can use the expression true as a catch-all head, which effectively gives us an "else" clause.

7.4   try ... catch etc

The try ... catch statement enables you to "protect" a block of code and to "catch" an exception if it occurs in that code.

Example:

test_try(Arg) ->
    try
        A = Arg + 3,
        {ok, A}
    of
        {ok, Result} ->
            io:format("Result: ~p~n", [Result]);
        Junk ->
            io:format("Junk: ~p~n", [Junk])
    catch
        error:Excp ->
            io:format("Excp: ~p~n", [Excp])
    end,
    io:format("finished~n"),
    ok.

Here is what we might see when we call this function:

17> c(test01).
{ok,test01}
18>
18> test01:test_try(25).
Result: 28
finished
ok
19> test01:test_try("abcd").
Excp: badarith
finished
ok

Notes:

  • The of keyword and block is optional.

  • If no exception occurs in the try block, the of block is given the result of the try block and can deal with it in a way that is similar to a case ... of statement.

  • If an exception does occur in the try block, then the of block is ignored and the exception is given to the catch block. If the catch block contains a clause that matches the exception, then that clause is evaluated and the exception is considered to have been handled. If no clause in the catch block matches the exception, then the exception "bubbles up" through the chain of function calls until it is either caught and handled (in another try ... catch statement) or is reported by Erlang.

  • Suggestion -- To find out what exception you should try to catch, you can mimic the exception in the Erlang shell. For example:

    23> throw(abcd).
    ** exception throw: abcd
    24>
    24> erlang:error("bad kittie").
    ** exception error: "bad kittie"
    25>
    25> 3 + "abc".
    ** exception error: an error occurred when evaluating an arithmetic expression
         in operator  +/2
            called as 3 + "abc"
    

    The characters that appear immediately after "** exception " are the name of the exception that you need to catch. In the above example, these exceptions could be caught with throw:Excp and error:Excp, where Excp is an unbound variable.

  • There is also plain catch and catch ... of, but its use is not recommended and should only be used in legacy code.

7.5   begin ... end

You can group a sequence of statements together with begin ... end. Here is an example:

31> begin
31>     io:format("one~n"),
31>     io:format("two~n"),
31>     io:format("three~n")
31> end.
one
two
three
ok

8   Processes

A process is an Erlang function that has been spawned. The process is "alive" as long as it continues to run.

We communicate with a process by sending messages to it.

A process receives messages by using the receive statement.

Example:

process_test() ->
        %Pid = spawn(fun () -> process_loop(0) end),
        Pid = spawn(?MODULE, process_loop, [0]),
        Pid ! {msg1, self(), "hello"},
        receive
                {ok, ReplyMsg1} ->
                        io:format("(process_test) received \"~s\"~n", [ReplyMsg1])
        end,
        Pid ! {msg2, self(), "goodbye"},
        receive
                {ok, ReplyMsg2} ->
                        io:format("(process_test) received \"~s\"~n", [ReplyMsg2])
        end,
        Pid ! stop,
        ok.

process_loop(Count) ->
        receive
                {msg1, FromPid, Msg} ->
                        io:format("~p. received message 1 -- Msg: \"~s\"~n", [Count, Msg]),
                        ReplyMsg = io_lib:format("processed message number ~B", [Count]),
                        FromPid ! {ok, ReplyMsg},
                        process_loop(Count + 1);
                {msg2, FromPid, Msg} ->
                        io:format("~p. received message 2 -- Msg: \"~s\"~n", [Count, Msg]),
                        ReplyMsg = io_lib:format("processed message number ~B", [Count]),
                        FromPid ! {ok, ReplyMsg},
                        process_loop(Count + 1);
                stop ->
                        io:format("exiting~n"),
                        ok
        end.

And, when we call function process_test/0, we see:

5> c(test01).
{ok,test01}
6> test01:process_test().
0. received message 1 -- Msg: "hello"
(process_test) received "processed message number 0"
1. received message 2 -- Msg: "goodbye"
(process_test) received "processed message number 1"
exiting
ok

Notes:

Rules and guidance for the receive statement:

9   ETS and DETS

9.1   ETS

ETS enables you to create an in-memory lookup table.

Create a table -- Here are some of your options:

  • named_table -- Enables you to refer to the table by the atom used to create it, rather than needing the process ID returned when it's created, thus enabling you to use the table in scattered parts of your code without passing the process ID.
  • set | ordered_set | bag | duplicate_bag -- Determines how many values per key, how the keys are ordered, etc.

For definitions of these options, see here: http://erlang.org/doc/man/ets.html#new-2.

Examples:

14> P1 = ets:new(t1, [set])
-> 28688
15> ets:insert(P1, {aa,11})
-> true
16> ets:insert(P1, {bb,22})
-> true
17> ets:insert(P1, {aa,101})
-> true
18> ets:lookup(P1, aa)
-> [{aa,101}]
19> f(P1)
-> ok
20> P1 = ets:new(t2, [bag])
-> 32785
21> ets:insert(P1, {aa,11})
-> true
22> ets:insert(P1, {aa,1001})
-> true
23> ets:lookup(P1, aa)
-> [{aa,11},{aa,1001}]

Notes:

  • The first table is a set. So, when we associate a second value with the key aa, only that second value is saved.
  • The second table is a bag. So, when we associate (insert) multiple values with the same key, all those associations are saved.

ETS has many more functions than those shown here. See the following for information about this library: http://erlang.org/doc/man/ets.html

9.2   DETS

DETS is very much like ETS, but DETS tables are stored in a disk file. This means that (1) they are slower and (2) they are persistent and can be accessed in a subsequent session.

** Create and use a DETS table** -- Use dets:open_file/3. See; http://erlang.org/doc/man/dets.html#open_file-1.

Examples:

27> dets:open_file(t1, [{file, "t1.dets"}, {type, bag}]).
{ok,t1}
28> dets:insert(t1, {aa, 1111}).
ok
29> dets:insert(t1, {aa, 1112}).
ok
30> dets:insert(t1, {aa, 1113}).
ok
31> dets:insert(t1, {aa, 1114}).
ok
32> dets:close(t1).
ok

And, possibly, much later:

33> dets:open_file(t1, [{file, "t1.dets"}, {type, bag}]).
{ok,t1}
34> dets:lookup(t1, aa).
[{aa,1111},{aa,1112},{aa,1113},{aa,1114}]
35> dets:close(t1).
ok

10   Mnesia

In order to use Mnesia, we need to do the following:

  1. Initialize Mnesia.
  2. Start your node.
  3. Create the schema.
  4. Start Mnesia
  5. Create the database tables.
  6. Populate the tables, make queries on the tables, etc.

There are many more Mnesia functions than the ones I discuss in this section. To learn about them, visit: http://erlang.org/doc/apps/mnesia/index.html.

Initialize Mnesia -- I use this script to start Mnesia:

#!/bin/bash
rlwrap -a -c -r \
    erl -mnesia dir '"/tmp/mnesia_data"' \
        -name session1@crow.local \
        -setcookie aaa

Create the schema -- Use this:

(session1@crow.local)3> mnesia:create_schema([node()]).

Start Mnesia -- In order to start Mnesia and get information about it, use this:

(session1@crow.local)1> mnesia:start().
ok
(session1@crow.local)2> mnesia:info().
---> Processes holding locks <---
---> Processes waiting for locks <---
---> Participant transactions <---
---> Coordinator transactions <---
---> Uncertain transactions <---
---> Active tables <---
schema         : with 1        records occupying 407      words of mem
===> System info in version "4.14", debug level = none <===
opt_disc. Directory "/tmp/mnesia_data" is used.
use fallback at restart = false
running db nodes   = ['session1@crow.local']
stopped db nodes   = []
master node tables = []
remote             = []
ram_copies         = []
disc_copies        = [schema]
disc_only_copies   = []
[{'session1@crow.local',disc_copies}] = [schema]
2 transactions committed, 0 aborted, 0 restarted, 0 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok

Define the records -- Since we will need these record definitions in several files, we'll put them in a separate include file -- mnesia_tables.hrl:

-record(recipe, {name, ingredients}).

-record(fruit, {name, color}).

Create the tables -- I used the following module:

-module(create_mnesia_tables).

-export([
         create_tables/0
        ]).

-include("mnesia_tables.hrl").

create_tables() ->
    mnesia:create_table(recipe, [{attributes, record_info(fields, recipe)}]),
    mnesia:create_table(fruit, [{attributes, record_info(fields, fruit)}]),
    ok.

Populate the tables -- Here is a module that can put some data in our tables:

-module(populate_mnesia_tables).

-export([
         populate_tables/0
        ]).

-include("mnesia_tables.hrl").

populate_tables() ->
    populate_recipe(),
    populate_fruit(),
    ok.

populate_recipe() ->
    Records = [
               #recipe{name=marmalade, ingredients=[sugar, mandarins, lemons]},
               #recipe{name=pancakes, ingredients=[flour, mil, yeast, eggs]}
              ],
    write_recs(Records),
    ok.

populate_fruit() ->
    Records = [
               #fruit{name=lemon, color=yellow},
               #fruit{name=strawberry, color=pink},
               #fruit{name=blueberry, color=pale_blue}
              ],
    write_recs(Records),
    ok.

write_recs([]) -> ok;
write_recs([Rec | MoreRecs]) ->
    F = fun() -> mnesia:write(Rec) end,
    mnesia:transaction(F),
    write_recs(MoreRecs).

Read the tables -- Now we can read data from our tables with the following:

(session1@crow.local)17> mnesia:transaction(fun () -> mnesia:read(fruit, lemon) end).
{atomic,[{fruit,lemon,yellow}]}
(session1@crow.local)18> mnesia:transaction(fun () -> mnesia:read(fruit, blueberry) end).
{atomic,[{fruit,blueberry,pale_blue}]}
(session1@crow.local)19> mnesia:transaction(fun () -> mnesia:read(fruit, watermelon) end).
{atomic,[]}
(session1@crow.local)20> mnesia:transaction(fun () -> mnesia:read(recipe, pancakes) end).
{atomic,[{recipe,pancakes,[flour,mil,yeast,eggs]}]}

11   Unit testing -- eunit

Basic use -- For simple use cases, eunit is quite simple.

Add this directive to your source code file immediately after the -module directive:

-include_lib("eunit/include/eunit.hrl").

Then every function whose name ends with "_test" or "_test_", becomes a test case.

Here is an example:

-module(test_gen_fsm_eunit).

-include_lib("eunit/include/eunit.hrl").

a1_test() ->
    ok = test_gen_fsm:test(),
    ok.

a2_test() ->
    nil = test_gen_fsm:test(),
    ok.

a3_test() ->
    ?assert(test_gen_fsm:test() =:= ok),
    ok.

a4_test() ->
    ?assert(test_gen_fsm:test() =:= none),
    ok.

Run the tests -- You can run all these tests with the following:

3> my_module:test().

Or, alternatively:

4> eunit:test(my_module).

And, you can get a bit more information by running:

5> eunit:test(my_module, [verbose]).

Macros -- The example above used the ?assert macro. You can learn about other eunit macros here: http://erlang.org/doc/apps/eunit/chapter.html, then look for "Macros".

Fixtures -- For more complicated uses, you will want to try eunit fixtures. Importantly, fixtures enable you to specify set-up and clean-up (tear-down), code to be run just before and after each of a set of tests.

An extensive and helpful set of slides containing many examples of the use of fixtures is here: http://www.erlang-factory.com/upload/presentations/33/EUnitinPractice.pdf

12   Hints, tips, and suggestions

12.1   Text editor support

Vim comes with a good Erlang mode.

See the following for additional plugins that are helpful for editing Erlang code in Vim: https://github.com/vim-erlang.

I believe that there is a good Erlang mode for Emacs.