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
, 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
that is used to increment the value of the counter.
The second clause contains the input statement
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
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
and , 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 , consist of executing run.
An active object outputs a term over
over channel C. Initially, N is zero.
When evaluating the goal
.ds a.g
an object , that is initialized with channel C.
The newly created object runs indepently of the process
evaluating the original goal.
Since both and 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 is evaluated by simplifying 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.