Intro to erlang

In this lab we'll try out erlang - a functional programming language with strong support for networking and distributed computation.

Lab Exercise

In your home directory create a file named
   erlang.cookie
In this file store a keyword you'll use to enable
   shared communications between otter and the pups
   (any unique name is fine)

This example assumes the sender is running on pup1 and
the receiver is running on otter.

Store the file as comms.erl,
then compile it using "erlc comms.erl"

Open a window on otter, and start erl using
   erl -sname talkRecver
In erl run
   comms:start_recv().

Open a window on pup1, and start erl using
   erl -sname talkSender
In erl run
   comms:start_send().
   
You should see a series of messages/acknowledgements 
   passed between the two.
-module(comms).
-export([sender/1, start_send/0, start_recv/0, recv/1]).

% create a macro that allows us to use ?ELSE as 
%    a catch-all for if/case statements
-define(ELSE, true).

% sends an initial greeting message
%   then waits for the acknowledgement
sender(SendMsg) ->
   { recvid, talkRecver@otter } ! SendMsg,
   io:format("greeting sent ~n"),
   receive
      ack -> 
         io:format("send got greeting ack ~n"),
         sender(normmsg);
      normack -> 
         io:format("send got message ack ~n"),
         sender(byebye);
      byeack ->
         io:format("send got hangup ack ~n");
      ?ELSE ->
         io:format("send got unknown msg ~n")
   end.
 
% waits for an incoming message and sends an
%   appropriate response (possibly calling itself
%   again to wait for subsequent messages
recv(RespMsg) ->
   receive
      greet -> 
         io:format("recv got greeting ~n"),
         { sendid, talkSender@pup1 } ! RespMsg,
         recv(normack);
      normmsg ->
         io:format("recv got message ~n"),
         { sendid, talkSender@otter } ! RespMsg,
         recv(normack);
      byebye ->
         io:format("recv got hangup ~n"),
         { sendid, talkSender@otter }  ! byeack;
      ?ELSE ->
         io:format("recv got unknown msg ~n")
   end.

start_recv() ->
   RECVID = spawn(comms, recv, [ ack ]),
   register(recvid, RECVID),
   io:format("receiver ~w ~n", [RECVID]).


start_send() ->
   SENDID = spawn(comms, sender, [ greet ]),
   register(sendid, SENDID),
   io:format("sender ~w ~n", [SENDID]).
Note that you can empty the message queue for a process using
   flush().

Note also that the 'human readable' form of PIDs is ,
   and there are routines to translate between the usuable form
   and the readable form:
PID = list_to_pid("<0.38.0>").
TextPID = pid_to_list(PID).

Processes can be registered under a name (atom) using
  register(chosenName, PID).
They can later be deregistered with
  unregister(chosenName).
You can look up the PID of a registered process with
  whereis(chosenName).
And you can get a list of registered processes using
  registered().
EXERCISE 1.1
(i)   Have the sending user enter the id (e.g. talkRecver@otter)
      of the person they wish to communicate with, and a block of
      text to be sent.
(ii)  Have the recv operation display the message received and
      who it came from.
(iii) Make the receives non-blocking by adding a timeout to
      the receive statement.  This is done by structuring 
      your receives like:
receive
   pattern1 -> pattern 1 actions;
     ...
   patternN -> pattern N actions;
   true    -> default actions
after 5000 -> timeout actions, 
              taken after 5000 milliseconds
end.

  • Once finished with the client-server exercise above, try working through and understanding the gen_server example below
    The calc module shown below provides a client server model for
    a simple calculator.  Compile the module then start erl with
       erl -sname server@localhost
    and to start the server use
       calc:start(). 
    
    Start up a client in another window with
       erl -sname client@localhost
    then have the client node connect to the server node with
       net_kernel:connect_node(server@localhost).
    
    You can then use the client window to run commands on the server, i.e.
        calc:addi(N).
        calc:divi(N).
        calc:subt(N).
        calc:times(N).
    The server-side calculator starts with an initial value of 0,
    and the operations above add, divide, subtract, or multiply
    the currently stored value by N (which can be any integer).
    
    The result of the operation is returned to the client,
    and the internal state of the calculator is updated.
    
    To stop the server run calc:stop(). 
    
    % calc.erl
    -module(calc).
    
    % will use gen_server as a template for the behaviour of our calculator
    -behaviour(gen_server).
    
    % gen_server requires we implement the following routines
    -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
    -export([code_change/3, start/0, stop/0]).
    
    % for our calculator we will supply add, subt, times, and divi
    -export([addi/1, times/1, divi/1, subt/1]).
    
    % instruct gen_server how to process calc:start(),
    %    which is what the user must run to initiate the calculator
    start()->gen_server:start_link({global, ?MODULE}, ?MODULE, [], []).
    
       %*******
       %** Aside: 
       %**   The use of the { global, ?MODULE } registers
       %**      your module name globally in much the same way
       %**      a register command would.
       %**   If you want to see a list of registered names, enter
       %**       global:registered_names().
       %*******
    
    % instruct gen_server how to process calc:stop(),
    %    which is what the user must run to shut down the calculator
    stop() -> gen_server:cast( { global, ?MODULE }, stop).
    
    % initially the step is 0 and the value held in the calculator is 0
    init([]) -> {ok, {0, 0}}.
    
    % instruct gen_server how to process calls to addi, subt, divi, and times
    addi(Num) -> gen_server:call(  { global, ?MODULE }, {addi, Num}).
    times(Num) -> gen_server:call(  { global, ?MODULE }, {times, Num}).
    subt(Num) -> gen_server:call(  { global, ?MODULE }, {subt, Num}).
    divi(Num) -> gen_server:call(  { global, ?MODULE }, {divi, Num}).
    
    % the calculator stores it's current { step, value },
    %    starting each at 0
    %    each call to addi(X), subt(X), divi(X), times(X) increments step
    %    and uses X and the old calculator value as the operands
    %    the returned value is reply, answer, updated calculator state
    handle_call({addi, Num}, _From, { Step, Value }) -> 
       {reply, Value+Num, { Step+1, Value+Num} };
    handle_call({times, Num}, _From, { Step, Value }) -> 
       {reply, Value*Num, { Step+1, Value*Num} };
    handle_call({subt, Num}, _From, { Step, Value }) -> 
       {reply, Value-Num, { Step+1, Value-Num} };
    handle_call({divi, Num}, _From, { Step, Value }) -> 
       {reply, Value/Num, { Step+1, Value/Num} };
    % the start() op begins the calculator with state { step 0, value 0 }
    handle_call({start}, _From, _State) -> 
       { reply, 0, { 0, 0 } }.
    
    % instruct gen_server how to handle a stop request
    handle_cast(stop, State) -> { stop, normal, State }.
    
    handle_info(_Msg, State) -> { noreply, State }.
    
    % behaviour to execute on calculator shutdown
    terminate(_Reason, _State) -> ok.
    
    % used if we want to hot-swap calculator code (not done here)
    code_change(_OldVersion, State, _Extra) -> { ok, State }.
    
    
    EXERCISE 1.2
    (i)   Add a calc:reset() option that resets the stored value to 0
              and resets the step number to 0.
    (ii)  Add a list to the tuple containing the server's state,
             and have the list record each of the operations performed.
    (iii) Have the terminate command print out a list of all operations
             performed, in order, since the server was started.
    

    Additional Resources