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 will
give the same results as evaluating the goal
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 ,
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
as in
?- travel!reachable(tokyo).
for which the answer is, perhaps unfortunately, no.
When the goal 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 ,
similar to the object travel but with (possibly) different
values for the non-logical variable city, which allows us to
ask
?- lib!member(O,[]), 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 are arbitrary terms.
The term is called the constructor,
since, when creating a new object,
a process is started to evaluate the goal .
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 ,
that define the own activity of each instance of an agency.
Suppose now that we wish to combine four travel agencies,
of the old kind into two new agencies,
then we may state as a goal
?-
O1 = new(agency([])),
O2 = new(agency([])),...
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 , 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.