Objects and processes

\label{des/ext/objects} We start by introducing the notion of objects. Throughout, a program is a Prolog-like program and a collection of object declarations. In its most simple form an object declaration looks as follows. \dlpindex{object}
  object name {
  clauses
  }
  
As we continue, we will gradually introduce features giving more functionality to an object.

Objects as modules

\dlpindex{objects as modules} The first view that we will take of objects is simply to regard them as modules, containing a collection of clauses. As an example of such an object, look at the declaration for a library of list manipulation predicates. \lprog{lib}{ [P object lib { member(X,[X|_]). member(X,[_|T]):- member(X,T). append([],L,L). append([H|T],L,[H|R]):- append(T,L,R). } } Clauses can be activated by a goal of the form \dlpindex{\callexpr} which is a request for the evaluation of goal using the clauses defined for the object with that name. An example of the use of such a goal could be ?- lib!member(X,[amsterdam, paris, london]). which, following ordinary Prolog evaluation rules, would bind the logical variable X successively to the cities mentioned in the second argument of member.

Method call

\dlpindex{method call} The intended semantics for an object as declared above does not deviate in any significant way from the semantics of an ordinary Prolog program. In other words, evaluating the goal lib!member(X,L) will give the same results as evaluating the goal member(X,L) when the clauses for member are directly added to the program. This holds in particular for backtracking. When a goal has multiple solutions, these solutions will be tried just as for an ordinary goal. .so com The obvious advantage of having the clauses for a predicate assembled in a module-like object is that, when a different functionality for these predicates is required, another object can simply be asked to do the evaluation.

Use

\dlpindex{use} We may extend the facilities for modular programming by allowing an object to use the clauses of another object. For example, when defining a predicate inboth(X,L1,L2), which checks whether X occurs both in list L1 and L2, it is convenient to be able to use the definition for member directly by using the clauses of lib, instead of explicitly addressing each call of member at the object lib. This is realized in the declaration for the object check. \lprog{check}{ .ds check.pl }

Objects with states

\label{des/ext/obj:states} \dlpindex{object with states} Modules of the kind treated above, however useful they may be, do not deserve to be classified as objects, since they do not contain any private data nor do they have an internal state. Below we will introduce non-logical variables, for which we allow destructive assignment.\ftn{ Non-logical variables are usually called instance variables in object oriented terminology. } In addition, we will introduce a facility to make instances, or rather copies, of declared objects. Furthermore, we will briefly discuss how objects may inherit non-logical variables and clauses from other objects.

Non-logical variables

\dlpindex{non-logical variables} Objects may contain private data. We introduce non-logical variables for storing such data. As an example consider the declaration for the object travel. \lprog{travel}{ .ds travel1.pl } We may ask such an object to evaluate the goal reachable(tokyo) as in ?- travel!reachable(tokyo). for which the answer is, perhaps unfortunately, no. When the goal reachable(tokyo) is evaluated we assume that the non-logical variable city is replaced by its value, the list of cities to which it is initialized. Moreover, because of the backtracking provided by Prolog, we could ask the object travel to list all reachable cities. The advantage of overloading predicate names becomes apparent when we imagine the situation in which we have a number of travel agencies, implemented by the objects travel _1,...,travel_n, similar to the object travel but with (possibly) different values for the non-logical variable city, which allows us to ask ?- lib!member(O,[travel_1,...,travel_n]), O!reachable(tokyo). that may after all get us where we want to be. Non-logical variables, that allow to store persistent data and that enable search over these data by backtracking, are of relevance for the implementation of knowledge based systems. For a small example it may not seem worthwhile to introduce non-logical variables, but in a real life situation the data may be stored in a large database. Only the clauses declared for an object have access to the non-logical variables. This justifies our speaking of clauses as methods, since the clauses provide an interface to the object encapsulating these data.

Assignment

\dlpindex{assignment} Having non-logical variables, the question immediately arises as to whether we may change the value of such a variable or not. It seems unnatural to have to answer no, since, for example, a travel agency may decide to change the service it offers now and again. We introduce a goal of the form \dlpindex{\simpleassignexpr} for assigning values to non-logical variables. The use of such a goal is illustrated in the following version of travel. \lprog{travel}{ .ds travel2.pl } So, as an example, when we have as a goal ?- travel!add(berlin). each successive request to travel includes berlin as a reachable city. For convenience we have assumed that the list of destinations always grows longer. In general, assignment to a non-logical variable is destructive, in that the previous value is lost.\ftn{ We will discuss the protection needed in the presence of concurrency in section \ref{des/ext/rendez}, where we treat the rendez-vous mechanism. }

Instances of objects

\dlpindex{instances of objects} Objects with mutable states require to have the possibility to create instances of objects of a particular kind. For example, we might wish to have a number of instances of the object travel, which differ in the destinations they offer. Each instance of an object contains both a copy of the non-logical variables of the object and a copy of its clauses. The non-logical variables of an instance are initialized to the current value of the non-logical variables of the object. Apart from the clauses declared for the object, a copy is also made of the clauses contained in the objects occurring in the use list. To create an instance of an object we introduce a goal \dlpindex{\simplenewexpr} that results in binding the newly created instance of the object to the logical variable O. Its use is illustrated by a goal like
  ?-
      O1 = new(travel), O2 = new(travel),
      O1!add(berlin), O2!add(antwerpen).
  
in which two instances of the object travel are created, which differ in that they respectively include berlin and antwerpen in their offer of reachable destinations. Notice that instances of objects are also objects.\ftn{ We have deviated from standard terminology, in not speaking of objects as instances of classes, since both the named object declared in the program and its instances (that is copies) may be used as objects. }

Inheritance

\dlpindex{inheritance} \dlpindex{use} \dlpindex{isa} As we have seen, an object may use the clauses of the objects contained in its use list. We propose another feature to enable an object to inherit the non-logical variables of other objects. This type of inheritance is exemplified in the declaration
  object travel {
  var city = [amsterdam, paris, london].
  }
  
  object agency {
  isa travel.
  ...
  }
  
This declaration ensures that the object agency, and all its instances, will have a non-logical variable city, initialized to the list above. In most cases the inheritance relation is such that the inheriting object contains both the non-logical variables and the clauses of the objects it inherits. We have introduced the notation \dlpindex{\inheritexpr} object a:b { ... } as a shorthand for
  object a {
  isa b.
  use b.
  ...
  }
  
As an example, consider the declaration below. \lprog{agency}{ [P object travel { use lib. var city = [amsterdam, paris, london]. reachable(X):- member(X,city). } object agency : travel { book(X) :- reachable(X), price(X,Y), write( pay(Y) ). price(amsterdam,5). ... } } The object agency may use all the clauses of travel, and in addition has access to the non-logical variable city. Inheritance is effected by code-sharing, in a static way. Conceptually, the inheriting object contains a copy of the objects it inherits. We will discuss how we deal with clashes that may arise in multiple inheritance in chapter \ref{des/know}, where we will also provide some examples of how inheritance may be used for knowledge representation.

Active objects

\dlpindex{active objects} So far, we have not given any clue as to how we will deal with concurrent programming in our (yet to be proposed) language. The first idea that comes to mind is to make passive (instances of) objects active, by letting them have activity of their own. Having a number of objects concurrently executing some activity of their own is, however, not of much help when there is no means to communicate with these objects. Thus, in addition to providing the means to create active instances of objects, it is also necessary to provide a way by which their activity can be interrupted in order to evaluate some goal. An active object is created by a goal of the form \dlpindex{\simplenewobjectexpr} where name is the name of the declared object, and t1,...,t_n are arbitrary terms. The term name(t1,...,t_n) is called the constructor, since, when creating a new object, a process is started to evaluate the goal name(t1,...,t_n). In order to avoid failure, clauses must be defined by which the constructor can be evaluated. The predicate name of the head of these clauses which, for obvious reasons we call constructor clauses, is identical to the name of the declared object. .so exec

Acceptance

\dlpindex{acceptance} The constructor clauses specify what may be called the body of an object, which determines its own activity. To interrupt this own activity we provide the goal \dlpindex{\acceptanyexpr} that forces the object to wait until it is requested to evaluate a goal.\ftn{ Later on we will encounter accept goals of a more complex nature. } When this has happened --that is when the goal is evaluated and an answer has been sent back-- the accept goal succeeds and the object may continue with its own activity. As an example, consider the object declaration for an agency that, in a naive way, implements the amalgamation of a number of travel agencies of the old kind. \lprog{agency}{ .ds agency1.pl } The declaration for agency differs from the declaration for the object travel only in having constructor clauses and an auxiliary clause for run(), that define the own activity of each instance of an agency. Suppose now that we wish to combine four travel agencies, travel1,...,travel4 of the old kind into two new agencies, then we may state as a goal
  ?-
     O1 = new(agency([travel1,travel2])),
     O2 = new(agency([travel3,travel4])),...
  
the result of which is that both agencies start with initializing their list of cities concurrently. The body of an agency consists, after initialization, of a tail-recursive loop stating the willingness to accept any goal. Each time the accept goal is reached, the object waits until it is requested to evaluate a goal. A request to evaluate a goal, in its turn, must wait until the object is willing to accept such a request.

Concurrency and synchronization

We have sketched here the simplest form of the evaluation of a goal by an object. We call this remote goal evaluation since we have not yet provided the means to be selective about what is acceptable as a request. Clearly, apart from the initialization and the fact that the own activity of an object must explicitly be interrupted, the semantics of an active object must be similar to that of a passive object. Conceptually, we may regard a passive object obj to be executing its constructor obj(), defined by obj() :- accept(any), obj(). In contrast with active objects, however, passive objects have unlimited internal concurrency as explained below.

Backtracking

\dlpindex{backtracking} A question we have not addressed when treating the remote evaluation of a goal by an active object is how to deal with the possible occurrence of backtracking over the resulting answers. Our approach is colored by our intention to have a semantics which coincides with that for ordinary Prolog, as far as backtracking is concerned. In our proposal we deal with the backtracking that may occur in a method call by creating a new process for each request to evaluate a goal. The backtracking information needed for finding all solutions for the goal is maintained by that process.

Internal concurrency

When multiple processes referring to a single object are active concurrently we speak of internal concurrency. For active objects we provide mutual exclusion between method calls in order to protect the access of non-logical variables. Mutual exclusion, however, restricts the degree of internal concurrency of an object. We do not wish to impose any restrictions on the internal concurrency displayed by passive objects. The programmer must take care to provide the protection necessary for safely accessing non-logical variables. Active objects allow only a limited form of internal concurrency, namely for backtracking over multiple answers to a method call.

Synchronization

We consider remote goal evaluation as an important means for objects to communicate with each other. Moreover, by requiring it to be stated explicitly whether an object is willing to accept a request, we have provided a means for synchronizing the behavior of objects. However, we may wish to be more selective in what to accept as a request. For instance, what is acceptable may depend on the state of the object, or even on conditions imposed on the arguments of the call. When the object is selective in this sense, it seems more apt to speak of a rendez-vous, since both the object and the process that requests the evaluation of a goal participate in establishing the communication. Summarizing, what we have described to this point is more or less a fully-fledged object oriented language. We may regard the clauses defined for an object as methods, having access to private data stored in the non-logical variables. Calling a method is to engage in a rendez-vous, when the object is willing to accept the call. Before continuing our description of this approach, however, we wish to reflect on the possibility of realizing objects with states that communicate by means of message passing. Do we need non-logical variables to implement states? And, do we need a synchronous rendez-vous to communicate with objects? We will deal with these questions in the next section, where we explore the possibility of using channels as the medium of communication between active objects.