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
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
References:
Archives