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