Communication by rendez-vous

\label{des/ext/rendez} In the previous section we have seen how we may implement objects with states without the use of non-logical variables to store the state of such objects. The approach sketched there has a number of limitations. Instead of augmenting the proposal of the previous section, we will take up the main thread of this chapter and will investigate how we may achieve object oriented behavior by regarding clauses as methods. Below we summarize the language features that we treat in this section. \dlpindex{\assignexpr} \dlpindex{\newobjectexpr} \dlpindex{\methodcallexpr} \dlpindex{\acceptexpr} \nop{ Note that we restrict ourselves to active objects. }

States

\dlpindex{states} As we have indicated in the section introducing objects we may use non-logical variables to store persistent data. We rephrase the declaration of the counter presented in the previous section to recall its use. \lprog{ctr}{ .ds ctr2.pl } This solution differs from the previous one in that the state of the object is not maintained by keeping the value of the counter as an argument in a tail-recursive loop, but as an explicit non-logical variable that can be updated by assignment.\ftn{ We allow for arithmetic simplifications both in the right-hand side of assignments to non-logical variables and in the left- and right-hand sides of an equality. } A typical use of such a counter is exemplified by the goal .ds ctr2.g In this a counter is created, which subsequently receives the method calls inc() and value(X). Despite the notational similarity with communication over channels, the calls C!inc() and C!value(X) are now method calls, that is goals that are evaluated by the object. The evaluation of these goals is taken care of by the clauses defined for inc and value.

Mutual exclusion

\dlpindex{mutual exclusion} Method calls for an active object must be explicitly accepted by an accept statement. To answer a method call an active object must interrupt its own activity. To protect the access to non-logical variables, mutual exclusion between method calls is provided by not allowing any method call to be accepted as long as the evaluation of a method call has not led to an answer being returned. An important question to answer is: when do we allow an object to continue with its own activity? In the absence of backtracking the natural choice is: immediately after the answer has been delivered. In the presence of backtracking we might wish to deliver all answers before allowing an object to continue its own activity. However this is overly restrictive since protection of non-logical variables is not really needed when backtracking over alternative solutions as shown in section \ref{des/ext/back}. Another thorny issue, which arises in the presence of backtracking, is what to do with non-logical variables that may be assigned values in an imperative way. Must these assignments be undone on backtracking or not? As the example of a counter shows, assignment to non-logical variables is of an imperative nature. Consequently, such assignments are not undone on backtracking! \nop{ An instance of a counter is an active object. Method calls for an active object must be explicitly accepted by an accept statement. To protect the access to non-logical variables, mutual exclusion between method calls is provided for active objects by not allowing any method call to be accepted as long as the evaluation of a method call has not led to an answer being returned. Having delivered an answer, other method calls may become active. As we will see, for passive objects, we will not wish to provide such protection. }

Suspension

\dlpindex{suspension} The mutual exclusion provided by the counter is meant only to protect the access to non-logical variables. At any time, any method call is acceptable. It is conceivable, however, that whether or not a method call is acceptable depends on the state of the object, as expressed in its non-logical variables. A typical example of such an object is the semaphore, given below. \lprog{sema}{ .ds sema.pl } The constructor for sema causes the semaphore to loop over a conditional that tests the value of the non-logical variable n. When the value of n is zero, calls to p() will be suspended, because of the statement accept(v); otherwise both p() and v() may occur, since when n is not zero the statement accept(p,v) is evaluated. A semaphore of the kind above may be used to regulate the concurrent evaluation of goals by passive objects. To illustrate this we present a modified version of the travel agency described in section 1.2. \lprog{travel}{ .ds travel3.pl } The object travel implements a multiple readers/single writers protocol, since adding an item to the city list is embedded in the semaphore calls p() and v(). The initialization of the non-logical variable s to an active instance of sema occurs exactly once for each instance of travel.

Semantics

Let us now take a closer look at the semantics of the accept statement. We only allow accept statements to occur in active objects since we wish method calls for passive objects to be evaluated concurrently.\ftn{ In addition, for active objects we do allow accept statements to occur (possibly nested) in processes for evaluating method calls. In our semantic description, however, we impose the requirement that accept statements may occur only in the constructor process of an object. } .so rv Operationally, when an accept statement is reached, the evaluation of the current goal is suspended until a method allowed by the arguments of the accept statement is called for. We call the argument of the accept statement the accept list, and by convention take any to stand for all methods of the object. When a method not occurring in the accept list is called for, the call is suspended and the object waits until another call satisfying the accept list occurs. The suspended call will result in a process evaluating the method call whenever the accept list is changed in such a way that the call is allowed. To handle suspended calls the object maintains a so-called accept queue. All suspended calls join in the accept queue, in the order in which they arrive. When the accept list is changed, the object first searches the queue and takes the first call satisfying the new accept list. If no such call is present the object waits for other incoming calls. This procedure guarantees fairness in handling method calls since because of the FIFO behavior of the accept queue, no allowed call will be suspended forever. C.f.  [Am89b]. .so phil