Communication over channels

\label{des/ext/channel} We may implement objects as continuously running processes communicating with each other over channels. C.f.  [ST83],  [PN84]. Before going into details we will present the language constructs involved. First of all we need a facility to create processes. We will use a goal of the form \dlpindex{\newprocessexpr} to create an active instance of the object c. To create new channels we use a goal of the form \dlpindex{\channelnewexpr} which results in binding the newly created channel to the logical variable C. Further we need, what we call an output statement of the form \dlpindex{\channeloutexpr} where C refers to a channel and t is an arbitrary term. Also, we need an input statement of the form \dlpindex{\channelinexpr} where C is assumed to refer to a channel and t is an arbitrary term.

Counter

We will characterize the semantics of communication over channels by giving a simple example, adapted from  [PN84], but originally given in  [ST83]. Assume that we wish to implement a counter that allows us to ask for its value and to increment its value. Clearly, we must have some means to store the state of the object, and also some means to send it the corresponding messages. With the constructs introduced, our implementation looks as follows. \lprog{ctr}{ .ds ctr1.pl } The first clause encountered is the constructor for an instance of ctr. The argument C is assumed to refer to a channel. Evaluating the constructor results in calling run(C,0), initializing the (logical) state variable holding the value of the counter to zero. The remaining two clauses define the body of the object. The first clause contains the input statement C?inc() that is used to increment the value of the counter. The second clause contains the input statement C?value(N) that is used to answer requests for the value of the counter. The value of the counter is maintained appropriately by passing it as an argument to the tail-recursive call to run. A typical example of the use of such a counter is the goal .ds ctr1.g that modifies the binding of X to one. The example given illustrates the use of such objects to implement server processes. Let us now give a more detailed description of the semantics of communication over channels.

Bi-directional unification

Communication over channels is synchronous, in that both sides wait until there are complementary communication requests for that channel. For the example above this means that the body of the counter will remain at the goal C?inc() until the user process reaches an output statement. We call a communication successful if the term on the input side, or more briefly the input term, is unifiable with the output term, the term on the output side. When these terms do not unify, as in the case for inc() and value(X), the input side is allowed to backtrack until it finds another input statement for that channel and the procedure is repeated. As long as the input side is backtracking the output side waits with its request to communicate. The asymmetry with respect to backtracking is exemplified above. We must remark, however, that Delta Prolog adopts a communication mechanism that is symmetric in its backtracking behavior but is rather complex. We stress that both in Delta Prolog and in our proposal communication over channels is bi-directional, in the sense that variables in both the input term and the output term may receive a value through unification. As an example consider the following object declaration .ds a.pl The body of the object a, which is defined by the clauses constructor a(C), consist of executing run. An active object outputs a term f(N,Y) over over channel C. Initially, N is zero. When evaluating the goal .ds a.g an object a(C), that is initialized with channel C. The newly created object runs indepently of the process evaluating the original goal. Since both C!f(N,Y) and C?(X,1) share the same channel, an attempt at communication takes place, which results in binding X to zero and Y in the body of a to one.

Sieve

We conclude this intermezzo with an example in which the number of processes can grow indefinitely large. Below we present our implementation of the solution to the problem of generating primes given in  [Ho78]. The solution consists of a chain of processes, the first of which -- called the driver -- produces natural numbers and the others -- representing the primes -- check for divisibility by a prime. The definition of the driver process is as follows. \oprog{driver}{ .ds driver.pl } The body of the driver produces an infinite sequence of (odd) natural numbers which are sent to the first sieve process. \lprog{sieve}{ .ds sieve.pl } A sieve contains a prime and checks all incoming numbers for divisibility by that prime. The first number received by a sieve process is known to be a prime. The process then creates a new sieve and checks all incoming naturals for divisibility by the prime it has stored. If the incoming natural is not divisible by the prime stored by the sieve it is sent to the successor process of that sieve. The output is collected by sending each prime with which a new sieve is initialized to a special process. The goal I // P != 0 is evaluated by simplifying I // P to I modulo P followed by a test as to whether the result is unequal to zero. The program is started by the goal .ds primes.g .so sieve As sketched here, communication over channels offers a rather limited functionality. In particular, since we have not included guarded commands or annotated variables, synchronization must rely purely on the synchronous nature of communication. Another important limitation is that no backtracking over the results of a communication is allowed, once a successful communication is achieved.