server know where to send the response, and the get_next
atom will let us differentiate this protocol operation from
others. The server responds with its own two-element
tuple: the server pid followed by the retrieved counter
value. Including the server pid lets the client distinguish
this response from other messages that might be sitting it
its mailbox.
A cast is a request to a server that needs no response,
so the protocol is just a request message. The reset/1 cast
has a request message of just a bare atom.
ABS TRAC TING PROTOCOLS
Brief as it is, the Erlang implementation of sequences is
much longer and less clear than the original Java version.
Much of the code is not particular to sequences, however,
so it should be possible to extract the message-passing
machinery common to all client-server protocols into a
common library.
Since we want to make the protocol independent of
the specifics of sequences, we need to change it slightly.
First, we distinguish client call requests from cast requests
by tagging each sort of request message
explicitly. Second, we strengthen the
association of the request and response by
tagging them with a per-call unique value.
Armed with such a unique value, we use
it instead of the server pid to distinguish
the reply.
As shown in figure 2, the server
module contains the same structure as
the sequence1 module with the sequence-specific pieces removed. The syntax
Module:function calls function in a module
specified at runtime by an atom. Unique
identifiers are generated by the make_
ref/0 primitive. It returns a new reference,
which is a value guaranteed to be distinct
from all other values that could occur in
the program.
The server side of sequences is now
boiled down to three one-line functions,
as shown in figure 3. Moreover, they
are purely sequential, functional, and
deterministic without message passing. This
makes writing, analyzing, testing, and
debugging much easier, so some sample
unit tests are thrown in.
S TANDARD BEHAVIOURS
Erlang’s abstraction of a protocol pattern is called a
behaviour. (We use the Commonwealth spelling, as that’s what
is used in Erlang’s source-code annotations.) A behaviour
consists of a library that implements a common pattern
of communication, plus the expected signatures of the
callback functions. An instance of a behaviour needs
some interface code wrapping the calls to the library plus
the implementation callbacks, all largely free of message
passing.
Such segregation of code improves robustness. When
the callback functions avoid message-passing primitives,
they become deterministic and frequently exhibit simple
static types. By contrast, the behaviour library code is
nondeterministic and challenges static type analysis. The
behaviours are usually well tested and part of the standard library, however, leaving the application programmer the easier task of just coding the callbacks.
Callbacks have a purely functional interface. Information about any triggering message and current behaviour
state are given as arguments, and outgoing messages
sequence2.erl (callback implementation)
-module(sequence2).
-export([make_sequence/0, get_next/1, reset/1]).
-export([init/0, handle_call/2, handle_cast/2]).
-export([test/0]).
3
API
make_sequence() -> server:start(sequence2).
get_next(Sequence) -> server:call(Sequence, get_next).
reset(Sequence) -> server:cast(Sequence, reset).
Server callbacks
init() -> 0.
handle_call(get_next, N) -> {N, N + 1}.
handle_cast(reset, _) -> 0.
Unit test: Return ‘ok’ or throw an exception.
test() ->
0 = init(),
{ 6, 7} = handle_call(get_next, 6),
0 = handle_cast(reset, 101),
ok.