Prolog++ toolkit - Details

How OOPS benefits from Logic

There are a number of advantages gained by using Prolog as a basis for an object-oriented language.

  • Prolog is a type free language (i.e. variables are not constrained to certain data-types). This allows greater freedom for the programmer than a typed language, leading to an increase in both productivity and creativity.
  • All data structures in Prolog are dynamic. The creation and garbage collection of data structures is automatic. A Prolog-based OOPS will reflect this by allowing truly dynamic class hierarchies.
  • In addition to these, Prolog++ may be fully integrated with standard Prolog programs. Each Prolog++ class has access to all Prolog predicates, whether system-defined or user-defined. This allows Prolog++ to inherit all the capabilities of a fully developed Prolog environment like list processing, unification, backtracking.

For example, consider a program which establishes animal taxonomies. The common features of animals may be abstracted into general classes and particular animals can then be classified according to those classes. In a Prolog-based OOPS not only can we dynamically add classes to a class hierarchy, we can dynamically augment classes that have already been established. This means that when a new characteristic is found for an existing animal, it can immediately be added to the class structure of that animal at run-time. This is not easily achieved in other systems.

Adding OOPS to Prolog

Programs may be developed using Prolog++ which augment standard Prolog in the following ways:

  • Prolog imposes very few restrictions on the structure and organisation of a program. This is fine for small scale programs, but can lead to problems when constructing large applications. The onus is on the programmer to impose some discipline for organising and managing their code. The ability to define a class hierarchy allows the programmer to partition Prolog programs into dynamic modules which helps organise a program and closely resemble the structure of the problem domain.
  • Each class within Prolog++ also has a clearly defined interface formed by the names of its methods which corresponds to its abstract or outline. The ability to define classes whose interface corresponds to their function is also of use when trying to model the problem domain.
  • The ability to have a close resemblance between a class, its interface and the problem domain enables the interfaces to the classes to be easily understood. This coupled with the encapsulation of the classes into packages of code and data allows Prolog++ programs to be re-used like building blocks.
  • Data-Driven programming is supported by allowing daemons to be attached to crucial events. The mechanism for trapping events is incorporated in the Prolog++ engine itself and does not need to be explicitly handled by the user.

Prolog as a logic-based language

Prolog is a declarative language. This means that the formal definition or declaration of a problem may be used as a functional program to solve that problem. In Prolog we declare the logical relationships of a given problem domain rather than state a step-by-step procedural recipe for solving the problem. This is useful as often we know various aspects of the problem but very little about how to find a solution. Prolog will attempt to find a solution for us given the information about the problem.

The way Prolog does this is by using a built-in inference engine, which automatically infers the solution to a given query using the facts and rules as defined in the program. Prolog programs consist of facts and rules, which are referred to as clauses. A fact consists of a single assertion with no conditions: a fact is always true. A rule consists of a goal, whose truth is dependent upon a set of conditions or sub-goals. The goal of a rule is referred to as its head and the sub-goals as its body.

Prolog attempts to prove a goal by using its backward chaining inference engine to match the initial goal with either a known fact or the head of a rule. If the goal is matched with a known fact the goal is proven. If the goal is matched with the head of a rule the original goal is then replaced with the sub-goals which form the body of the rule. Prolog then attempts to prove, in the same way, each of these sub-goals in turn.

Although Prolog programs are thought of as declarative they can also have a procedural reading. Prolog is versatile: there is a style of Prolog programming which mimics the conventional procedural approach, but with less emphasis on the actual assignment statements.

Prolog as a logic programming language has a sound theoretical basis, being modelled on the first-order predicate calculus. This means that theoretically a Prolog program, which may be read as a set of axioms and theorems, contains the possibility of being proven consistent or inconsistent, regardless of the implementation. In this way Prolog can be thought of as an automated theorem prover.

Variable Types

A feature of Prolog is its use of type free variables which can represent widely different data structures. This allows any variable in a clause to represent a number, a string, a list of numbers, a list of strings, a list of strings and numbers etc. Prolog variables can even be used to represent clauses. This last use of variables leads to a technique called meta-programming, where the clauses in one part of a program can manipulate other clauses as data. This 'meta-level' ability is one reason why Prolog has been widely used in the realm of expert systems.

Data Structures

Another feature of Prolog is its ability to allow the dynamic assertion and retraction of rules and facts. This makes the Prolog rule and data base truly dynamic at run-time. This is essential for "learning systems" and other applications involving the introduction of new concepts, rules or facts at run-time.

Memory is dynamically allocated and deallocated at run-time. The deallocation is automatically done by a built-in garbage collector. The programmer does not have to be concerned with the implementation, maintenance and bookkeeping normally associated with the creation and destruction of complex dynamic data structures and is freed to concentrate on stating the logic of the problem.

Prolog is now in widespread use in industry and the ISO Prolog standard was published in 1995. The Prolog Management Group was formed in 1993 to promote and highlight the various commercial applications fo Prolog.

Interfacing to external code and data

Prolog++ has direct access to Prolog, which in turn has support for procedures written in C, C++ and Pascal. This is platform specific, but, for instance, under Windows 95 and 3.1, there is extensive support for DLLs and DDE. LPA Prolog also has a comprehensive set of formatted I/O routines for accessing external structured data files, as well as a dedicated interface to most commercial databases via ODBC. This ability to integrate rules and data is essential in fielded systems.

Development facilities

At development time there are a range of aids and support facilities provided by Prolog++. These differ from platform to platform but generally include:

  • object-browser to interactively browse classes and instances
  • methods editor to interactively edit and compile methods
  • object-graph to graphically display and interact with the classes

History and availability

Prolog++ is a mature product and was developed by Logic Programming Associates Ltd (LPA) in 1989 for 640K MS-DOS PCs. This accounts for its efficient and compact run-time system. Prolog++ is now available as an LPA product on Windows 95, Windows 3.1, Macintosh and MS-DOS machines.

1994, Addison-Wesley published "Prolog++: The Power of Object-Oriented and Logic Programming" by Chris Moss. In 1995, LPA released v2.0 which introduced part-of hierarchies to supplement is-a hierarchies and other extensions.

Notation

As a brief introduction to some of the notation used within Prolog++ the following table is given. This is not intended to be an exhaustive set of all the available symbols, but rather an initial set to illustrate the sophistication of the system.

Method/Arity

Methods are referred to by the / symbol, with the name of the method on the left and its arity (number of arguments) on the right.

Function//Arity

Functions are referred to by the // symbol, with the name of the function on the left and its arity (number of arguments) on the right.

Attribute

Attributes are referred to by their name alone.

Instance <- message

<- is the principal symbol of prolog++, and indicates that the message on the right is to be sent to the instance on the left.

Instance@Attribute

@ is the symbol used to indicate the value of an instance's attribute.

Program Statements

The program statements occurring between the open and close statements define the structure and contents of a class. They fall into two distinct categories:

  1. code statements which define how various kinds of messages are handled
  2. meta statements which talk about the class and its methods

The code statements can be further divided into procedural and functional method definitions. The meta statements cover the areas of class hierarchies, part-of hierarchies, encapsulation and dynamic data. These statements can occur in any order whatsoever, with code and meta information freely intermixed.

Logical variables

Prolog++ uses the same terminology for logical variables as Prolog (e.g. Element, Stack).

Instance variables

Prolog++ provides two instance variables represented by specially reserved words of the language. They can be used in any context whatsoever, having the same syntactic status as any other Prolog term. The instance variables provided by Prolog++ are:

  1. self substituted at run-time by the instance, or class, to which the message being handled was originally sent
  2. super substituted at compile-time by the super class of the class in which it textually occurs

Is-a hierarchy

The is-a hierarchy of Prolog++ relates specific classes with more general classes through the mechanism of inheritance. The is-a relationship between classes is expressed within each class definition by declaring its super or parent class(es).

class stack.
   inherits buffer             % inherits all characteristics of buffers
   ...
end stack.

class queue_with_statistics.
   inherits queue,             % inherits from general queues
   buffer_with_statistics      % as well as statistical buffer
   ...
end queue_with_statistics.

A strict hierarchy is one in which all classes have either a single super class or none at all. Such a hierarchy is said to exhibit singular inheritance. Hierarchies in which at least one class has several super classes is said to exhibit multiple inheritance.

The is-a hierarchy can be navigated by referring to super, sub, ancestral and descendant classes. This is expressed by a collection of reserved words:

  • super_class - the/a super or parent class
  • sub_class - a sub or child class
  • ancestor_class - a super class (parent), or a super-super class etc.
  • descendant_class - a sub class (child), or a sub-sub class (grandchild), etc.

Part-of hierarchy

The part-of hierarchy of Prolog++ relates class instances with oneanother such that they can be thought of either collectively or individually. The creation of a complex object such as a bicycle may involve the creation of several parts, including wheels, a frame and handlebars. These in turn may involve the creation of further sub-parts such as spokes and rims. The part-of relationship between class instances is expressed within each class definition by declaring its component parts.

class bicycle.
   parts frame, wheel * 2, seat, handle_bars.
end   bicycle.

class unicycle.
   inherit bicycle.
   parts wheel, handle_bars * 0.
end   unicycle.

class tandem.
   inherit bicycle.
   parts seat * 2, handle_bars * 2.
end   tandem.

The parts declaration can be inherited. For example, in addition to the 2 seats and 2 sets of handle-bars a tandem will inherit a frame and 2 wheels from the bicycle class. The unicycle forces the handle-bars not to be inherited by explicitly declaring 0 for that part.

Composite instances

Whenever a new instance of a class is created the parts declaration of that class is inspected and the corresponding instances of the sub-parts are created. For example, consider the various cycle classes above and assume that the others are all sub-classes of cycle_component.

class cycle_component.
public method when_created/0.
when_created :-
   write( 'Created: ' ),
   write( self ),
   nl.
end cycle_component.

Accessing Part-of hierarchy

The part-of hierarchy can be navigated by referring to sub parts, super parts and the top-most ancestral part. This is expressed by a collection of reserved words:

  • composite_part - the top-most instance in this part-of hierarchy
  • super_part - the super or parent instance which this is a part of
  • sub_part - a sub or child part of this instance
  • # - a specifically numbered sub part.

Attributes

The attributes of a class correspond to the characteristics of the entity which it represents. They may or may not have default values. Attributes can be split into those which relate to individual instances of the class and those for the overall class itself. Each of these can be declared as private or public attributes.

A public attribute is one whose value is accessible from outside the class in which it occurs, whereas a private attribute can only be accessed from within. In either case, the attribute's value can only be changed from within the class.

public class attribute
   smallest .                 % the smallest instance in
            .                 % this class; no default value

private instance attributes
   contents = [] , .          % default contents is empty list
   size is 0 , .              % the default size is zero
   cumulative_size inherited .% cumulative size is inherited
            .                 % from an ancestor class

An instance attribute is one which relates to an individual instance of a class. For example, consider a class representing customers of a bank. One attribute is the amount of work necessary to service each individual customer. A class attribute is one which relates to the overall class itself. For example, an atribute of a class of customer queues may be a pointer to the smallest queue. Each attribute in the declaration list can be assigned an explicit default value (arithmetic or non-arithmetic), an inherited default value or have no default.

Assignment

There are two forms of attribute assignment in Prolog++, one which activates procedures and the other which does not. An assignment which invokes the daemon manager is referred to as a noisy assignment, and one which merely performs the update and does nothing else is referred to as a quiet assignment. They both have the same basic form:

  1. "Name of Attribute" := "New Value"
  2. "Name of Attribute" :== "New Value"

where := is an example of a noisy assignment operator and :== is an example of a quiet assignment operator.

A Case Study In Fault Diagnosis

This case study will investigate the application of Prolog++ to the diagnosis of faults. It is intended to illustrate the technique of progressively sending messages through the hierarchy, starting from at a general level and grdually moving down to classes at the lower, more specific levels. The hierarchy is used to represent the implicit relationships between different classes of faults.

The Problem

We would like to represent the causal-effect relationship between faults and symptoms. The particular domain, that of car maintenance, was chosen to illustrate the techniques in layman terms. Everybody is familiar, to a greater or lesser degree, with the problems that can occur with automobiles.

The crucial factor in fault diagnosis is being able to quickly pinpoint the general area of the problem, before focusing in on the root cause. Identifying general aspects of the problem can avoid going down blind alleys, and more importantly avoids asking the user seemingly irrelevant questions.

For the domain of car maintenance, an automobile can be dissected into several problem areas such as the fuel system, mechanical faults and electrical faults. Associated with each area is a collection of faults which may occur, and the symptoms which they cause. Some of the symptoms may be contradictory, in the sense that two symptoms cannot possibly occur simultaneously.

The Classes

From the above discussion two quite separate types of instance can be identified. The first concerns itself with fault diagnosis, including the ability to move from general terms down to specific terms. The second deals with the problem domain, in terms of the causal-effect relationships between actual faults and exhibited symptoms.

The following example illustrates a hierarchy for identifying faults in automobiles. At the topmost level is the faults class containing the algorithm for finding faults, and which is inherited by all of the other classes in the hierarchy. At any point, however, a class has the option to override the default search algorithm with one that is specialised for its problem area.

The following sections define the protocol (attributes, methods and functions) for the faults instance and for the domain specific objects.

The Domain Classes
class   mechanical.
inherit fault.
end     mechanical.

class   engine.
inherit mechanical.
end     engine.

class   cylinders.
inherit engine.
end     cylinders.

class   fuel_system.
inherit fault.
end     fuel_system.
class   electrical.
inherit fault.
end     electrical.

class   lights.
inherit electrical.
end     lights.

class   starting.
inherit electrical.
end     starting.

class   starter_motor.
inherit starting.
end     starter_motor.

class   sparking.
inherit starting.
end     sparking.

class   plugs.
inherit sparking.
end     plugs.

class distributor.
inherit sparking.
method  fault    // 1.
   fault(   f1001 ) = 'Condensation in the distributor cap'.
   fault(   f1002 ) = 'Faulty distributor arm'.
   fault(   f1003 ) = 'Worn distributor brushes'.
method  symptom  // 1.
   symptom( s1001 ) = 'The starter turns but the engine doesnt fire'.
   symptom( s1002 ) = 'The engine has difficulty starting'.
   symptom( s1003 ) = 'The engine cuts out shortly after starting'.
   symptom( s1004 ) = 'The engine cuts out at speed'.
method  effect   // 1.
   effect(  f1001 ) = s1001.
   effect(  f1002 ) = s1001.
   effect(  f1002 ) = s1004.
   effect(  f1003 ) = s1002.
   effect(  f1003 ) = s1003.
method  contrary /  2.
   contrary( s1002, s1001 ).
   contrary( s1003, s1001 ).
end distributor.
The fault Class
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% CLASS  : fault                                                  %
% COMMENT: How to search for and report faults                    %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

class fault .

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                           DECLARATIONS                          %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

category
   fault ,                  % A class in the fault diagnosis
   user .

public methods
   findall         /  0 .   % Find and report all possible faults

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                           DEFINITIONS                           %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% METHOD : findall/0                                              %
% COMMENT: Find and print all possible faults                     %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

findall :-
   dynamic( told_by_user / 2 ),
   forall (
           ( Where = self
           ; Where = descendant_class ),
           Where <- find( fault )
          )
   do     (
           write( 'location      : ' ), write( where ), nl,
           write( 'possible fault: ' ), write( fault ), nl, nl
          ).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% method : find/1                                                 %
% comment: find a possible fault for some class                   %
%          explicit use of 'self' to avoid any early bindings!    %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

find( fault ) :-
   public( fault // 1 ),
   fault = self@fault( faultnumber ),
   forall symptomnumber = self@effect( faultnumber )
   do     exhibited( symptomnumber ).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% method : exhibited/2                                            %
% comment: ask the user whether or not the symptom is             %
%          exhibited & remember                                   %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

exhibited( symptomnumber ) :-
   told_by_user( symptomnumber, reply ),
   !,
   reply = yes.

exhibited( symptomnumber ) :-
   cat( [self@symptom(symptomnumber),'?'], question, _ ),
   ( yesno( question ) -> Reply = yes ; Reply = no ),
   asserta( told_by_user( SymptomNumber, Reply ) ),
   Reply = yes,
   forall ( self <- contrary( symptomnumber, contrarysymptom )
          ; self <- contrary( contrarysymptom, symptomnumber ) )
   do     asserta( told_by_user( contrarysymptom, no ) ).

end fault.

The Output

The following outputs were taken from a fault diagnosis session.

?- faults <- findall .

location       : distributor
possible fault : condensation in distributor cap

location       : distributor
possible fault : faulty distributor arm
no more solutions