figure 3: 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]).
API
make_sequence()
get_next(Sequence)
reset(Sequence)
-> server:start(sequence2).
-> server:call(Sequence, get_next).
-> 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.
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 se-quence-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 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.
figure 4: Parallel call implementations.
a
Make a set of server calls in parallel and return a
% list of their corresponding results.
Calls is a list of {Server, Params} tuples.
multicall1(Calls) ->
Ids = [send_call(Call) || Call <- Calls],
collect_replies(Ids).
Send a server call request message.
send_call({Server, Params}) ->
Id = make_ref(),
Server {call, {self(), Id}, Params},
Id.
Collect all replies in order.
collect_replies(Ids) ->
[receive {Id, Result} -> Result end || Id <- Ids].
B
multicall2(Calls) ->
Parent = self(),
Pids = [worker(Parent, Call) || Call <- Calls],
wait_all(Pids).
worker(Parent, {Server, Params}) ->
spawn(fun() -> create a worker process
Result = server:call(Server, Params),
Parent {self(), Result}
end).
wait_all(Pids) ->
[receive {Pid, Result} -> Result end || Pid <- Pids].
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
standard Behaviours
Erlang’s abstraction of a protocol pattern is called a behaviour. (We use the
Commonwealth spelling as 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 and a new state are
given in the return value. The process’s
“eternally looping function” is implemented in the library. This allows for
simple unit testing of the callback
functions.
Large Erlang applications make