This in an Erlang Workshop by Lmabda Academy.
Table of Contents
$ git clone https://github.com/lambdaacademy/erlang-workshop.git
$ cd erlang-workshop
First create an Erlang application:
$ ./rebar3 new lib name="abacus" desc="Erlang Abacus"
rebar3 is an Erlang build tool. It has built in help:
./rebar3 help
.
Move rebar3
into the abacus
directory that was created. From now on, all
the actions will be performed in that directory.
Edit the src/abacus.erl
and create 4 functions for arithmetic operations:
addition/2
subtraction/2
multiplication/2
division/2
.
Save the files, compile, and run an Erlang shell:
$ ./rebar3 compile
$ ./rebar3 shell
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V8.1 (abort with ^G)
1> abacus:addition(2,3).
5
With
rebar3 shell
the recommended way to quit the Erlang shell is to either useinit:stop().
orq()
as Ctrl+C Ctrl+C breaks your OS shell formatting.
If you haven't done previous step, start with git checkout 1-create-project-and-1st-module
.
Create the test
directory and add test/abacus_tests.erl
starting with
-module(abacus_tests).
-include_lib("eunit/include/eunit.hrl").
EUnit is a Lightweight Unit Testing Framework for Erlang.
Write 4 unit tests for the abacus
functions implemented in the first step:
add_test() ->
?assertEqual(2+3, abacus:addition(2,3)).
%% ...
Save and run the tests with rebar3
:
$ ./rebar3 eunit
Finished in 0.026 seconds
4 tests, 0 failures
If you haven't done previous step, start with git checkout 2-create-tests
.
Edit rebar.config
and enable cover:
{cover_enabled,true}.
Cover is a Coverage Analysis Tool for Erlang.
Run the eunit tests again to generate the coverage data. Then perform the analysis and open the report:
$ ./rebar3 do eunit, cover -v
Finished in 0.026 seconds
4 tests, 0 failures
|------------------------|------------|
| module | coverage |
|------------------------|------------|
| abacus | 100% |
|------------------------|------------|
| total | 100% |
|------------------------|------------|
$ open _build/test/cover/index.html
If you haven't done previous step, start with git checkout 3-perform-code-coverage-analysis
.
Edit abacus.erl
to add module and the API functions documentation
in the EDoc format:
%% @author Szymon Mentel <[email protected]>
%%
%% @doc This is the Abacus API module.
%%
%% It provides basic mathematical operations:
%% <li>addition,</li>
%% <li>subtraction,</li>
%% <li>multiplication</li>,
%% <li>division</li>.
-module(abacus).
%% ...
%% @doc Adds two integers
%%
%% This adds `X' to `Y'.
addition(X, Y) ->
X+Y.
Edoc is the Erlang documentation generator.
Save. Generate and open EDoc documentation.
$ ./rebar3 edoc
$ open doc/index.html
If you haven't done the previous step, start with git checkout 4-add-documentation
.
Add src/abacus_srv.erl
. Implement abacus_srv:start/1
and abacus_srv:stop/1
API functions that will spawn and kill the abacus_srv
process respectively.
start(Name) ->
Pid = spawn(?MODULE, init, [Name]),
register(?Module, Pid).
stop(Name) ->
exit(whereis(Pid), shutdown).
Implement the addition/3
, subtraction/3
, multiplication/3
, division/3
functions in the abacus_srv
module that will send messages to the abacus_srv
process which in turn will call corresponding functions from the abacus
module
and send the response back the the caller:
addition(Name, X, Y) ->
Request = {addition, X, Y},
send_request(Name, Request).
send_request(Name, Request) ->
Name ! {abacus_request, self(), Request},
receive
{abacus_resopnse, {ok, Result}} ->
Result;
{abacus_resopnse, {error, _} = Error} ->
Error
after 5000 ->
{error, timeout}
end.
Save, compile and test the server:
./rebar3 shell
...
1> abacus_srv:start(a1).
<0.95.0>
2> abacus_srv:addition(a1,10,3).
13
3> abacus_srv:division(a1,10,3).
3
4> whereis(a1).
<0.95.0>
5> abacus_srv:stop(a1).
true
6> whereis(a1).
undefined
If you haven't done the previous step, start with
git checkout 5-create-a-server
.
Add asynchronous version of the abacus
functions to the src/abacus_srv.erl
(async_addition/1
, ...) and an API for retrieving the results of asynchronous
calls: abacus_srv:result_by_reference/1
. The asynchronous functions are
expected to return references (see make_ref/1) which are to be used to get
the results:
async_addition(Name, X, Y) ->
Request = {addition, X, Y},
send_async_request(Name, Request).
send_async_request(Name, Request) ->
Ref = make_ref(),
Name ! {abacus_async_request, Ref, Request},
Ref.
result_by_reference(Name, Ref) ->
Request = {result_by_reference, Ref},
send_request(Name, Request).
The server's process needs to store the results of asynchronous requests in its
state. Extend the loop/0
and response/1
functions to handle the asynchronous
requests and retrieving the results respectively:
loop(State) ->
receive
...
{abacus_async_request, Ref, Request} ->
Response = response(Request, State),
NewState = store_response(Ref, Response, State),
loop(NewState);
...
end.
...
response({division, X, Y}, _State) ->
abacus:division(X, Y);
response({result_by_reference, Ref}, State) ->
retrieve_response(Ref, State).
Save, compile and test the server:
./rebar3 shell
...
1> abacus_srv:start(a).
<0.100.0>
2> Ref1=abacus_srv:async_addition(a,13,2).
#Ref<0.0.1.465>
3> Ref2=abacus_srv:async_subtraction(a,13,2).
#Ref<0.0.1.471>
4> abacus_srv:result_by_reference(a,Ref1).
15
5> abacus_srv:result_by_reference(a,Ref2).
11
6> abacus_srv:result_by_reference(a,make_ref()).
unknown_reference
If you haven't done the previous step, start with
git checkout 6-add-asynchronous-interface
.
Copy the abacus_srv.erl
into abacus_gen_srv.erl
. Make it gen_server
behaviour (check what behaviour is) :
%% @author Szymon Mentel <[email protected]>
%%
%% @doc This is the Abacus Server.
-module(abacus_gen_srv).
-behaviour(gen_server).
Change the life-cycle API to use respective gen_server
functions
and change the init/1
to match the specification.
Also add the terminate/2
according to the specification.
start(Name) ->
ServerName = {local, Name},
CallbackModule = ?MODULE,
InitArgs = [],
Opts = [],
gen_server:start(ServerName, CallbackModule, InitArgs, Opts).
stop(Name) ->
gen_server:stop(Name).
init(_) ->
{ok, #{}}.
terminate(_Reason, _State) ->
ok.
Change the synchronous API to use appropriate gen_server
API and implement
corresponding callback functions:
addition(Name, X, Y) ->
Request = {addition, X, Y},
gen_server:call(Name, Request).
handle_call(Request, _From, State) ->
Response = response(Request, State),
{reply, {ok, Response}, State}.
Change the asynchronous API to use appropriate gen_server
API and implement
corresponding callback functions:
async_addition(Name, X, Y) ->
Request = {addition, X, Y},
Ref = make_ref(),
gen_server:cast(Name, {Ref, Request}),
{ok, Ref}.
handle_cast({Ref, Request}, State) ->
Response = response(Request, State),
NewState = store_response(Ref, Response, State),
{noreply, NewState}.
Don't forget to export the gen_server
callback functions:
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
terminate/2]).
Save, compile and test the server:
./rebar3 shell
...
1> abacus_gen_srv:start(a).
{ok,<0.95.0>}
2> abacus_gen_srv:multiplication(a,12,3).
{ok,36}
3> {ok,Ref}=abacus_gen_srv:async_division(a,12,3).
{ok,#Ref<0.0.2.656>}
4> abacus_gen_srv:result_by_reference(a,Ref).
{ok,4}
5> abacus_gen_srv:stop(a).
If you haven't done the previous step, start with
git checkout 7-make-it-gen_server
.
Add abacus_sup.erl
module that will implement the supervisor
behaviour:
-module(abacus_sup).
-behavoiur(supervisor).
Implement the supervisor start_link/0
function that will start the supervisor:
start_link(AbacusName) ->
SupervisorName = {local, ?MODULE},
CallbackModule = ?MODULE,
InitArgs = [AbacusName],
supervisor:start_link(SupervisorName, CallbackModule, InitArgs).
Implement the init/1
callback function that configures the supervisor itself
and starts its children:
init([AbacusName]) ->
SupFlags = #{intensity => 5, %% max 5 restarts
period => 10, %% in 10 seconds
strategy => one_for_one},
AbacusStart = {abacus_gen_srv, start_link, [AbacusName]}, %% MFA
Child = #{id => abacus_gen_srv,
start => AbacusStart,
restart => permanent,
shutdown => 1000,
type => worker,
modules => [abacus_gen_srv]},
Children = [Child],
{ok, {SupFlags, Children}}.
As the supervisor requires that its children link back to it, change the
abacus_gen_srv:start/1
to abacus_gen_srv:start_link/1
and adjust the
call to gen_server
:
start_link(Name) ->
ServerName = {local, Name},
CallbackModule = ?MODULE,
InitArgs = [],
Opts = [],
gen_server:start_link(ServerName, CallbackModule, InitArgs, Opts).
Save, compile and test the supervisor:
$ ./rebar3 shell
...
6> abacus_sup:start_link(server1).
{ok,<0.109.0>}
7> abacus_gen_srv:addition(server1,9,10).
{ok,19}
8> whereis(server1).
<0.110.0>
9> supervisor:which_children(abacus_sup).
[{abacus_gen_srv,<0.110.0>,worker,[abacus_gen_srv]}]
10> abacus_gen_srv:stop(server1).
ok
11> whereis(server1).
<0.116.0> %% note the different pid
12> exit(whereis(server1), kill).
true
13> whereis(server1).
<0.119.0> %% note the different pid
If you haven't done the previous step, start with
git checkout 8-supervise
.
Add the abacus_app.erl
: the callback module for application behaviour.
Implement the abacus_app:start/2
which will start the application's
top-level supervisor and an "empty" abacus_sup:stop/1
function:
-module(abacus_app).
-behavoiur(application).
%% Life-cycle API
-export([start/2,
stop/1]).
start(_Type, _Args) ->
abacus_sup:start_link(server1).
stop(_) ->
ok.
Modify the abacus.app.src
(Application Resource File) so that the
application is properly started:
{application, abacus,
[{description, "Erlang Abacus"},
{vsn, "0.1.0"},
{registered, []},
{applications,
[kernel,
stdlib
]},
{env,[]},
{modules, []},
{mod, {abacus_app, []}}, %% <- the missing line
{maintainers, []},
{licenses, []},
{links, []}
]}.
Instead of hard-coding the abacus server name as server1
, pass it through
the application environment variables using application:get_env/3
:
-define(DEFAULT_ABACUS_NAME, server1).
start(_Type, _Args) ->
Application = abacus,
Key = server_name,
ServerName = application:get_env(Application, Key, ?DEFAULT_ABACUS_NAME),
abacus_sup:start_link(ServerName).
Finally add the config/abacus.config
file with the following content:
[{abacus,
[{server_name, abs}]
}].
Save, compile and test the application:
./rebar3 shell --config config/abacus.config
1> application:ensure_all_started(abacus).
{ok,[abacus]}
2> whereis(abs).
<0.100.0>
3> abacus_gen_srv:division(abs, 12, 5).
{ok,2}
4> application:stop(abacus).
ok
5> whereis(abs).
undefined
Start the application once again (as described above) and run Observer:
observer:start().
In Observer navigate to Applications
-> abacus
. This will show
the supervision tree of the abacus
application.
If you haven't done the previous step, start with
git checkout 9-package-as-application
.
Modify the rebar.config
file and add lager dependency
that will be used for logging:
{erl_opts, [debug_info, {parse_transform, lager_transform}]}.
{deps, [
{lager, "~> 3.2"}
]}.
The
{parse_transform, lager_transform}
is the compiler option needed by lager.
Modify the abacus.app.src
file so that the lager
application
is started before abacus
:
...
{applications,
[kernel,
stdlib,
lager
]},
...
Add some logging to your code-base:
init(_) ->
{registered_name, Name} = erlang:process_info(self(),
registered_name),
lager:info("Started abacus_gen_srv: ~p", [Name]),
{ok, #{}}.
handle_call(Request, _From, State) ->
lager:debug("Handling sync request: ~p", [Request]),
Response = response(Request, State),
{reply, {ok, Response}, State}.
Configure lager
handler along with the log levels in the
config/abacus.config
file:
[
{abacus, [
{server_name, abs}
]},
{lager, [
{handlers, [
{lager_console_backend, info},
{lager_file_backend, [{file, "log/debug.log"}, {level, debug}]}
]}
]}
].
Save, compile and test the application as in the previous step.
If you haven't done the previous step, start with
git checkout 10-add-external-dependency
.
Add the following relx section in the rebar.config
:
{relx, [
{release, {abacus, "0.1.0"}, [abacus]},
{include_erts, true}
]}.
Save. Build the release and start it:
./rebar3 release
...
===> release successfully created!
17:25:24.648 [info] Application lager started on node 'abacus@szm-mac'
17:25:24.652 [info] Started abacus_gen_srv: abs
17:25:24.652 [info] Application abacus started on node 'abacus@szm-mac'
Eshell V8.1 (abort with ^G)
(abacus@szm-mac)1> application:which_applications().
[{abacus,"Erlang Abacus","0.1.0"},
{lager,"Erlang logging framework","3.2.1"},
{goldrush,"Erlang event stream processor","0.1.8"},
{compiler,"ERTS CXC 138 10","7.0.2"},
{syntax_tools,"Syntax tools","2.1"},
{stdlib,"ERTS CXC 138 10","3.1"},
{kernel,"ERTS CXC 138 10","5.1"}]
(abacus@szm-mac)2> abacus_gen_srv:multiplication(abs, 13, 4).
{ok,52}
(abacus@szm-mac)3>