Let’s start by looking at the first two lines.
-module(pingpong).
-compile(export_all).
The first is the statement of the module. Its argument is an atom (a word without quotation marks, in lower case) that represents the name given to the module. Adapted from Learn You Some Erlang:
-module(Name).
This is always the first attribute (and first sentence) of a file, and with good reason: it is the name of the current module, where Name
is an atom. This is the name you will use to call functions from other modules. Calls are made in the form M:F(A)
, where M
is the name of the module, F
is the function, and A
the arguments of this.
The second sentence tells the compiler to make public all functions defined in the module, ie the whole function F that write in this module can be called from outside, with pingpong:F
. This simplifies the learning process, but in general it is not good practice to make all public functions. Instead, list each function you want to export.
Let us now look at the defined functions.
start_pong() ->
register(pong, spawn(pingpong,pong,[])).
This is possibly the entry point for your code. Compile the module, and start by calling pingpong:start_pong().
in the Erlang console, in an instance of the virtual machine (a node). What this function does is to record the name pong
as an identifier for a new process that will be created, with spawn
.
Therefore, spawn
creates Erlang processes. spawn
is a function built-in (BIF), and therefore does not require you to write the name of the module in the prefix. Your arguments are spawn(Modulo, Funcao_Exportada, Lista_de_Argumentos)
, as seen in the documentation.
Returning to start_pong
, what it really does is create a process that will run the function pong
of this module, without arguments, and call to this process pong
.
pong() ->
receive
finished ->
io:format("Pong finished ~n");
{ping, Ping_Pid} ->
io:format("i am the receiver ~n"),
Ping_Pid ! pong,
pong()
end.
The new process of start_pong
will run this function. All Erlang process has its own mailbox. Processes communicate with each other by leaving messages in the boxes of others. Messages can be almost anything, any data.
The new process enters the block receive
, that tells you to search for messages in your mailbox, or to wait until there is one. Then use Pattern matching to find the action corresponding to the received message. If you are used to common imperative languages, you can see this almost as a switch.
If the process has a message composed of the atom finished
, he prints "Pong finished" on the console and ends. If the process has a message that is a pair composed of the atom ping
and a process identifier (pid - the whole process has its), then it will execute the remaining code of this function.
The Ping_Pid
, Starting with a capital letter, tells Erlang to store in a variable with this name whatever the value comes in the second element of the message. In this case, we’re just waiting for one pid.
When you enter this case, you print "i am the receiver" and sends a message with the atom pong
for the process identified by Ping_Pid
- this is the utility of the operator !
. Finally, the function is called recursively, to go back to the mailbox.
The next thing you’ll write on the console, probably in another instance of the virtual machine, will be the call to start_ping
.
start_ping(Pong_Node) ->
spawn(pingpong, ping, [3, Pong_Node]).
As seen before, what this does is create a new process, which will run the function ping
with arguments 3
and Pong_Node
, where the second is the node where the first process is running.
ping(0, Pong_Node) ->
{pong, Pong_Node} ! finished,
io:format("Pong finished ~n");
ping(N, Pong_Node) ->
{pong, Pong_Node} ! {ping, self()},
receive
pong ->
io:format("i am the sender ~n")
end,
ping(N-1,Pong_Node).
This function is defined in two cases (note that the first definition of ping
ends with ;
, instead of .
- this tells Erlang that there is still more to complete the definition of this function).
The function is called with 3
as first argument. How 3
does not amount to 0
, the process executes the second case, with N
as an argument.
This process sends the pair {ping, self()}
to the process given by {pong, Pong_Node}
, which follows the summary {nome_registado, nome_do_nodo}
. The function self()
is used to obtain the pid of the current process. After this, the process expects response, and repeats this cycle, while N
is greater than zero.
When N
reaches zero, the first case is executed by sending finished
at the {pong, Pong_Node}
, and ending execution.
If you find this explanation incomplete, you can also take a look in the tutorial, which explains this same programme.