Integrating Prolog Services with C++ Objects

(This article was originally published in PC AI magazine, Volume 9, Number 3 May/June 1995. The magazine can be reached at PC AI, 3310 West Bell Rd., Suite 119, Phoenix AZ, USA 85023 Tel (602) 971-1869, FAX: (602) 971-2321.)

This article uses a classic Prolog programming example to illustrate how Prolog back-end services can be integrated with C++ front-end user interface code. The sample application is genealogy, with Prolog providing rules for family relationships and C++ the GUI interface to the application. The connection between the two is encapsulated in a C++ class that provides application-specific Prolog services to the rest of the C++ application.

The Prolog Code

For this application the Prolog code provides four types of logic services to the host application. The first two are basically the same type of services a database can provide. They are: 1) accessing and manipulating the attributes of a person, and 2) maintaining a collection of persons in a particular family tree. The second two services draw on Prolog's strength for rule-based programming. They are: 1) answering queries about relationships in the data, and 2) providing semantic integrity checks on entered and updated data. For those who haven't spent much time in the database world, semantic integrity checks go beyond normal input- field validation. In this case, for example, they check to see if a person in the database is their own ancestor. If that is true, the individual data records might be valid, but the database does not make semantic sense as a genealogical tree.

Let's look at these four sections.

Person Data

This sample genealogical code has been written many times for many beginning Prolog books. While they all have the same types of rules for relationships, those rules are always based on some fundamental information. The choice of what is fundamental and what is derived varies from program to program. For this version of the program, I've decided the primitive fact will be person/5, where the five arguments are: Name, Gender, Mother, Father, Spouse. Because this is a decision that might change in the future, only a few primitive relationship rules are defined using the knowledge of the basic person information.

These relations are:

By building all of the other rules on these basic building blocks, it becomes easy to add or modify the basic data structure. For example, if we wanted to keep the date-of-birth for a person, that change would only impact these basic definitions.

Family Database

The next service provided by the Prolog program is maintenance of the collection of persons in a family. These predicates add a new person, delete a person, load the person database from file, and save it back to file. The add/5 predicate is designed to back out an update on backtracking, so that it can be easily used by the semantic integrity checking add_person/5 predicate.

Like the rules providing access to individual person data, these rules provide access to the family database. For this example, the person data is stored in the Prolog database, using asserts and retracts.

Relationship Query Rule-Base

We now get to the rule-base portion of the application. These query rules allow more complex information to be derived from the data. These are all the classic rules for grandparents, siblings, uncles, aunts, ancestors, etc. Some examples of these rules are shown here. Notice that they all build on the same set of primitive information so that changes to the underlying structure of the data in the application will not affect the relationship rule-base.

The query rule-base has one additional fact that can be used by the host program. It contains a list of all the defined relationships.

It can be used in a generic relationship rule that can find out how two people are related, among other things.

Semantic Integrity Rule-Base

The final service the Prolog module provides is semantic integrity checking. This means checking a proposed update or change to the database against the rest of the database to ensure it makes sense. Examples include checking: a person's mother and father are the right gender, someone is not their own ancestor, and spouses are not blood relatives. While this last is not really a genealogical rule, I've included the rule as an example of relatively complex semantic integrity checking on a database.

Here are some of the rules from the integrity rule-base portion of the Prolog code. Because this Prolog code is designed to be passively called from a host application, error messages are simply asserted to the database for the host program to retrieve in case an update fails. (Alternatively it could have been designed to take a more active role in the application, calling the host program directly when an error occurs.)

Summing Up the Prolog Code

As we can see from the above discussion, the Prolog code for this application is in four distinct sections. Two sections clearly behave as objects. These are the individual person object, and the collection of persons, or family object. The rules that manipulate these objects are all localized in the code. The other two sections are rule-bases that reason over the objects. One provides queries looking for relationships in the collection, and the other provides queries that verify relationships between persons are as they should be. In the next section we'll talk about encapsulating the entire Prolog program and its services as a C++ object that can be part of a larger application.

Calling Prolog Services from C++

For this example, the Prolog services will be accessed from a Windows C++ program that provides a GUI interface to the user. The GUI interface will let the user load and save family databases, use list-boxes to pose relationship queries, and use dialog-boxes to update the family database.

C++ is well suited to GUI programming, as it allows for the natural encapsulation of the various objects that comprise the interface. In this example, there are objects that correspond to each of the dialogs the application has with the user.

C++ is also well-suited for interfacing with Prolog, as it enables the application programmer to implement an object that encapsulates the Prolog services. In this way the C++ interface to the Prolog services is well defined, and the implementation can be maintained without impacting the rest of the application.

The example code was implemented using Microsoft Visual C++, the Microsoft Foundation Classes, and the Amzi! Prolog Logic Server API, however, these same ideas apply to any C++ implementation, GUI tool-kit, and Prolog with a host language interface.

The C++ Header Information

A C++ header file defines the interface to C++ classes. Because the program maps person information between C++ and Prolog, the C++ program first defines a structure called Person.

It will be used to hold and pass person information in the C++ portion of the program. (CString is a string class defined in the MFC. Other C++ vendors provide similar classes.)

Next, consider the class that defines the interface to the Prolog portion of the code. It is called ProGene, and in this case is derived from a vendor-provided class that provides generic Prolog services. The ProGene class extends those generic services with functions particular to this specific Prolog program.

Because the Prolog services might be available to a number of other classes, a global object is created for the Prolog object. When the application starts up, the object is created, and when the application closes down, the object is destroyed. This is all done automatically by one line of code in the beginning of the program.

Using the ProGene Class

Let's now look at some of the code that makes use of this interface.

First a family file must be opened. This is done by using the File/Open menu choice of the application which causes the following code to be executed. The code uses the familiar Windows file open dialog to let the user select a file containing a family tree. That file name is then passed to the Prolog server, which opens the file.

Once the database is open, it is ready to be queried about family relationships. The query dialog-box contains the three list-boxes shown in the screen image. The first list-box is initialized with the names of the individuals in the family, and the second with the possible relationships. The third list-box is filled with the results of queries constructed from choices in the first and second list-boxes.

In each case, the query dialog simply passes a pointer to a list- box to the proGene server object, which then populates the list- box with the appropriate information.

The queries are posed by selecting a name and relationship from the first two list-boxes. That data is passed to the Prolog server, which then populates the 'answers' list-box with the returned relatives. Because all of the information for the list- boxes is derived from the Prolog program, any changes to the Prolog data, additions of new relationships, etc. is all done in Prolog.

The last point of interest is the update dialog. After the user has entered the fields of a new person record, this function maps the dialog-box data to C++ variables, in this case the person structure defined earlier. This example includes some of the automatic field validation capabilites of the Microsoft Foundation Classes, but these validation edits only apply to field specific attributes such as length of a name, value of a number etc.

The full semantic integrity checking is provided by the 'SaveValid' function of the proGene Prolog server. This function passes the proposed new record to Prolog to either add to the database, or report an error causing the user to have to try again.

Notice that these code fragments simply maintain the user interface widgets of the application. They put up list-boxes, respond to buttons, get selected values from list-boxes, but nowhere have any "understanding" of what is contained in the user interface. That information is provided entirely by the Prolog portion of the application.

Conversely, the Prolog code has no "understanding" of the type of user interface being employed. It provides the services asked of it.

The user interface C++ code and rule-based Prolog code are cleanly isolated from each other, with the proGene server object providing the communication between them.

The Prolog Server

Finally, we'll look inside the Prolog server object to see how it connects C++ to the genealogical Prolog code. This particular example uses the Amzi! Prolog Logic Server API functions to call Prolog from C, but similar code can be implemented with any Prolog that provides a C interface.

The open is accomplished by sending a 'consult' query to the Prolog engine with the name of the file (containing a family tree) to be opened.

The persons list-box is populated by finding all answers to the Prolog query 'person(X)' and adding those answers to the list-box.

The relations list-box is similarly populated by getting the list of all relationships (from the relations/1 fact we saw earlier) and popping elements from the list into the list-box.

The answers list-box works similar to the persons list-box. In this case a more complex query is posed, and then redone until all of the answers have been retrieved and placed in the answer list- box.

The final piece to look at here is the code that performs the semantic integrity checking and update of the database. It also builds a Prolog query that calls add_person/5, which either adds the record or posts an error message. This code picks up the error message if the integrity checks fail, displays it for the user and then lets the caller deal with the error situation.

These are the highlights of the Prolog server interface that provide a clean insulating layer between the Prolog code containing the basic logic of the application and the user interface code written in C++.

Conclusion

This sample application illustrates how the object-oriented capabilities of C++ can be used to create a clean bridge between C++ code and Prolog code. The resulting design allows C++ to be used for those aspect of an application its best suited for, and Prolog for those aspects its best suited for. C++ is used with Microsoft's GUI tools to develop a Windows front end, while Prolog is used as both a database and a rule-base. The architecture makes it easy to maintain the logic rules of the application, coded in declarative Prolog, and the GUI interface using the increasingly sophisticated GUI tools available from C++ vendors. The link between the two components is neatly encapsulated in a C++ object.