TAP Tutorial

TAP, Thick Ada-Prolog bindings.

version 0.1, August 2002

Alexander Kopilovitch
aek@acm.org,   aek@vib.usr.pu.ru

This tutorial presents a complete example of the TAP (Thick Ada-Prolog) bindings basic usage, with the detailed explanations at each step. Note, though, that only basic features are explored in this example, and if you want more then you should get across the TAP Manual.

A reader is assumed familiar with the Ada basics as well as with the Prolog basics. Also, an acquaintance with Amzi Logic Server (www.amzi.com) -- at least with the Overview in its "Logic Server User's Guide & Reference" is desirable, although perhaps not strictly necessary for the first reading. Throughout this tutorial the language name Ada means Ada 95.

This is a Windows-oriented version of the Tutorial. For Unix/Linux environment all command files and LSX building technique is different from those shown here.

The Example.

1. The Problem Statement.

Let's consider the following problem statement:

There are two old gentlemen dining every day in a club. These gentlemen are very similar to each other in their tastes and habits. So it is quite natural that they always have their dinner together, at the same table for two persons. The notable thing about them is their strong tradition to read magazines during eating, and change the magazines with the next dish. A waiter puts a pile of different magazines before them along with the first dish, and the diners take them one by one, always picking the topmost one, and never returning a used magazine to the pile, but simply dropping it on the floor. The combination of a particular dish with a particular magazine either pleases or annoys the diner. Our problem is to find (and display) all the courses within a dinner for which the participant moods are different.

Additionally we assume for our dealings with the problem, that a diner is pleased if the first word of the current dish's name has a common letter with the current magazine's title, and is annoyed otherwise.

2. Formalization.

In order to formalize the above problem statement, we shall specify the basic entities, then introduce the derived, high-level entities, and then formulate in the terms of those entities the question to be answered.

The basic entities are represented by three external predicates: dish/1, magazine/1, and mood/3. The predicate dish/1 is called for a course change; it succeeds until the end of meal is reached, then it fails. On success, it bounds its argument (which must be a variable) to the next dish name. The predicate magazine/1 similarly bounds its argument to the name of the next topmost magazine from the pile. The predicate mood/3 accepts a dish name in its first argument, a magazine name in its second argument, computes the appropriate mood name, and bounds the latter to its third argument.

The second and third stages -- an introduction of high-level entities abd formulating of the question go differently in Ada and Prolog and in their various mixtures. In our problem this is mostly because of rather different facilities for looping in these languages. In a Prolog program (or the Prolog part of the program) the high-level entities will basically look similar to the following:

course(D,M,X) :- dish(D),
                 magazine(M1),
                 mood(D,M1,X1),
                 magazine(M2),
                 mood(D,M2,X2).                      
The following rules facilitate the displaying of the results (the backquote sign inside them is the delimiter for a literal string in Amzi Prolog)

display_course(X1, X2, D, M1, M2) :- display_dish(D),
                                     display_context(X1, X2, M1, M2),
                                     nl.

display_dish(D) :- write(` dish=`), write(D), nl.

display_context(X1, X2, M1, M2) :- write(`   magazine_1=`), write(M1),
                                   write(` mood_1=`), write(X1), nl,
                                   write(`   magazine_2=`), write(M2),
                                   write(` mood_2=`), write(X2), nl.

The question to be answered in interactive mode (that is, in the Prolog Listener) would be formulated as the following Prolog query:

?- course(D,M1,X1,D,M2,X2),
   X1 \= X2,
   display_course(X1, X2, D, M1, M2),
   nl.
(which should be retried until failed, for obtaining all cases of different moods, as required by the problem statement), but automatic looping, needed in the compiled mode, brings some decorations within and around that code.

You may look at the pure Prolog (p_dinner.pro) and pure Ada (a_dinner.adb) implementations of the task. You may even run those programs (go to the directory Samples, run command files p_build.bat and a_build.bat, and then run p_run.bat command file -- for Prolog or a_dinner.exe program -- for Ada).

For Ada, the specification of the basic entities is defined by the following Ada package Dinner_Services (unary operation "+" is defined in the Prolog package, and renames To_Unbounded_String function):

with Prolog; use Prolog;
with Prolog.Library;

package Dinner_Services is

  function Get_Dish
    return Boolean;
    
  function Get_Magazine
    return Boolean;
    
  function Determine_Mood
    return Boolean;
    
  PTable : constant Predicate_Table := (
                    (+"dish",     1, Pure_Predicate, Get_Dish'Access),
                    (+"magazine", 1, Pure_Predicate, Get_Magazine'Access),
                    (+"mood",     3, Pure_Predicate, Determine_Mood'Access)
                                        );
                                        
  package LSX is new Prolog.Library(PTable);
  
end Dinner_Services;
Let us review the above package spec in detail. The spec consists of three kinds of items: first, the set of the executor function prototypes for the predicates; second, the predicate table -- the array of specifications of the predicates for the Logic Server; third, the instantiation of the generic package Prolog.Library, which immerses the initatialization subroutines, which are needed when the external predicate stuff is encapsulated in a separate library (that is, Windows DLL or Unix shared object).

An executor function prototype should always be in the form used above, that is, a parameterless function returning Boolean. The function acquires the values of predicate's arguments and assigns new values to them using the subroutines Get_Parameter and Set_Parameter. Different predicates may share the same executor -- so, the number of executor functions generally may be less than the dimension of the predicate table.

Within the predicate table declaration, the first place in a row is for the predicate's functor, that is, its name inside a Prolog program. The "+" prefix at the predicate's name provides the transformation to Unbounded_String (the "+" function is defined inside the Prolog package).

The second place in a predicate table's row is for the predicate's arity, that is, the number of the predicate's arguments inside a Prolog program.

The third place in a predicate table's row is for the predicate's kind. The Pure_Predicate value here means that the predicate does not participate in Prolog backracking (for other possible predicate kinds see TAP Manual).

The fourth (last) place in a predicate table's row is for the predicate's executor. An access to a parameterless Boolean Ada function must be provided here.

An instantiation of the Prolog.Library generic package is required only if there is an intention to encapsulate the external predicates into a Logic Server Extension, that is into a Windows DLL or Unix shared object. But it does no harm to do so anyway, even if the package Dinner_Services will be used within the main program. Note that the name of the predicate table is the necessary argument to that instantiation.

3. Implementation.

There are two issues pertaining to the implementation: first, the function bodies for the external predicates must be provided, and second, the form of the final application along with the place for the external predicates in it must be decided and realized. The first issue is quite conventional, but for the second one a short introductory overview of the possibilities is desirable.

There is two-staged choice for the form of an application as it relates to the Logic Server. For one of them we must decide whether the main program of the application will be a Prolog program, which in this case contains a query in the form of a Prolog goal, or the main program will be Ada program, which queries the Prolog program using the Logic Server API. If the second mode is chosen then we must decide for another option: whether the external predicates will be encapsulated into a Logic Server Exetension, that is, a Windows DLL or Unix shared object, or they will be part of the main Ada program. So, we can create 3 different configurations:

  1. Prolog main program that uses the external predicates encapsulted in a Logic Server Extension, contains a goal, and displays the results;
  2. Ada main program with the external predicates inside it;
  3. Ada main program with the external predicates encapsulated in a Logic Server Extension (and therefore initialized from the Prolog program).

In two latter cases the Prolog program does not contain a goal, but is queried via the Logic Server API by the main Ada program. All three just described configurations will be developed here below.

Now let us consider the implementation of the external predicates (for all configurations it is the same). First, we provide the package Dinner_Supply, which serves as a store, that is, provides a meal and magazines for a dinner:

with Prolog; use Prolog;  -- needed here for VString and "+" declarations only,

package Dinner_Supply is

  Meal : array(Positive range <>) of VString := (
                                                 +"black pudding",
                                                 +"crab soup",
                                                 +"steak & kidney pie",
                                                 +"lemon tart"
                                                );

  Pile : array(Positive range <>) of VString := (
                                                 +"Quizionary",
                                                 +"Nose To Nose",
                                                 +"Myself",
                                                 +"General Dissent",
                                                 +"Veteran's Opinion",
                                                 +"Corporate Science Weekly",
                                                 +"Truth, Wealth, Health",
                                                 +"Bribery Times"
                                                );

end Dinner_Supply;
Then, using the above package we implement all needed predicates. Here is the code:

with Prolog; use Prolog;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Dinner_Supply; use Dinner_Supply;
with Ada.Text_IO; use Ada.Text_IO;

package body Dinner_Services is

  Run : Session;

  Course_Number : Natural := Meal'First - 1;
  Top : Natural := Pile'First - 1;
  
  function Get_Dish
    return Boolean
  is
    Current_Dish : VString;
  begin
    Course_Number := Course_Number + 1;
    if Course_Number <= Meal'Last then
      Current_Dish := Meal(Course_Number);
      Set_Parameter(1,
                    Current_Dish,
                    Name);
      return True;
    else
      return False;
    end if;
  end Get_Dish;
  
  function Get_Magazine
    return Boolean
  is
    Topmost_Magazine : VString;
  begin
    Top := Top + 1;
    if Top <= Pile'Last then
      Topmost_Magazine := Pile(Top);
      Set_Parameter(1,
                    Topmost_Magazine,
                    Name);
      return True;
    else
      return False;
    end if;
  end Get_Magazine;
  
  function Determine_Mood
    return Boolean
  is
    Dish, Magazine, Mood : VString;
    Dish_Space : Integer;
    Dish_Word : Unbounded_String;
  begin
    -- get input arguments
    Dish     := Get_Parameter(1);
    Magazine := Get_Parameter(2);
    -- pick the first word in the dish name
    Dish_Space := Index(Source => Dish, Pattern => " ");
    if Dish_Space > 0 then
      Dish_Word := Head(Dish, Dish_Space - 1);
    else
      Dish_Word := Dish;
    end if;
    -- compute mood
    Mood := +"bad";  -- assume
      -- change to the "good" if the magazine's title and the picked part of
      -- the dish name have a common letter 
    for I in 1 .. Length(Magazine) loop
      exit when Element(Magazine, I) = ' ';
      if Index(Source => Dish_Word, Pattern => Slice(Magazine, I, I)) > 0 then
        Mood := +"good";
      end if;
    end loop;
    -- bound the third argument to the computed mood
    Set_Parameter(3,
                  Mood,
                  Name);
    -- always succeed
    return true;
  end Determine_Mood;
  
end Dinner_Services;
At this point we are able to compose the first of 3 possible configurations -- one with Prolog main program and the external predicates encapsulated in a Logic Server Extension. The Prolog program is a combination of the parts -- the rules and the goal (along with result display stuff):

course_body(D, M1, M2, X1, X2) :- dish(D),
                                  magazine(M1),
                                  magazine(M2),
                                  mood(D, M1, X1),
                                  mood(D, M2, X2).
                                  
course(D, M1, M2, X1, X2) :- course_body(D, M1, M2, X1, X2),
                             assert(consumed(D)),
                             X1 \= X2.

course(D, M1, M2, X1, X2) :- retract(consumed(U)), course(D, M1, M2, X1, X2).

% ----------------------------------------------------------------------

main :- course(D, M1, M2, X1, X2),
        display_course(X1, X2, D, M1, M2),
        fail.

display_course(X1, X2, D, M1, M2) :- display_dish(D),
                                     display_context(X1, X2, M1, M2),
                                     nl.

display_dish(D) :- write(` dish=`), write(D), nl.

display_context(X1, X2, M1, M2) :- write(`   magazine_1=`), write(M1),
                                   write(` mood_1=`), write(X1), nl,
                                   write(`   magazine_2=`), write(M2),
                                   write(` mood_2=`), write(X2), nl.
This Prolog program must be compiled and built with the Amzi Prolog compiler. The result, which represents the application for a caller, is the file Dinner.xpl .

The Logic Server Extension in the form of Windows DLL Dinner.dll can be produced from the package Dinner_Services (that is, the pair Dinner_Services.ads and Dinner_Services.adb) presented above, with the following commands:

gnatmake -c -aI..\Specs -aL..\Lib Dinner_Services
gnatdll -d Dinner.dll -e ..\Bin\AdaLSX.def -I..\Lib -I..\Bin Dinner_Services.ali -n
where the file AdaLSX.def is supplied with these bindings, and has the following contents:

EXPORTS
    InitPreds=InitPreds@8
    LSAPI_Ready=LSAPI_Ready@8
After that, the utility "starter" program arun.exe (which is a slightly extended version of the arun.exe program from the Amzi Prolog distribution) can run the whole application, with the command:

..\bin\arun Dinner.xpl
provided that the text file Dinner.cfg is present in the current directory, and contains the line:

LSXLOAD <full path to the dinner.dll>
(the Samples directory contains the command file runXPL.bat for the run, along with the Dinner.cfg configuration file, which provides an API trace also).

Now let's turn to the other two possible configurations of the application. In both of them the main program is an Ada program, and that main program queries the Prolog code using the Logic Server API. The only essential difference between them is that in one of them the Dinner_Services package (Dinner_Services.ads, Dinner_Services.adb) is included into the main program, while in the other one that package participates in the application in the form of DLL -- exactly the same way as for the above case of the Prolog main program.

Both configurations are represented by the code below, where the (GNAT-specific) preprocessor is used for their separation. There are two significant differences in the code, and preprocessor actuates appropriate lines according to the Boolean value of Predicates_Here symbol: if Predicates_Here is True then the first configuration is choosen, thus package Dinner_Services is referenced in "with" clause, and the predicate table is submitted to the Open function; otherwise, package Dinner_Services is not referenced, and No_Extended_Predicates constant is submitted to the Open function instead of the predicate table, along with the predicate library filename Dinner.dll as a configuration parameter. There are also two minor differences -- at the beginning and at the end of the subroutine -- the configured versions are given slightly different names for convenience. Here is the code:

with Ada.Text_IO; use Ada.Text_IO;
with Prolog; use Prolog;

#if Predicates_Here

with Dinner_Services;
procedure Example1

#else

procedure Example2

#end if;

is
   Run : Session;
   Got_One : Boolean;
begin
   Run := Prolog.Open ("Dinner.xpl",
#if Predicates_Here
                       Dinner_Services.PTable
#else
                       No_Extended_Predicates,
                       +"Dinner.dll"
#end if;
                      );
   Got_One := Apply (Run, 
                     "course(D,M1,M2,X1,X2).",
                     Multi_Step => True);
   while Got_One loop
      New_Line (2);
      -- display all variables in the name=value form
      Put_Line (S(Display_All (Run)));
      New_Line; 
      -- then present "formatted" display
      Put_Line (" dish=" & S(Display_Variable (Run, "D")));
      Put_Line ("   magazine_1=" & S(Display_Variable (Run, "M1"))
                & " mood_1=" & S(Display_Variable (Run, "X1")));
      Put_Line ("   magazine_2=" & S(Display_Variable (Run, "M2"))
                & " mood_2=" & S(Display_Variable (Run, "X2")));
      Got_One := Proceed (Run);
   end loop;
   Close (Run);

#if Predicates_Here
   
end Example1;

#else

end Example2;

#end if;
These two configurations may be built using the sets of commands:
gnatprep -DPredicates_Here=True example.adb example1.adb
gnatmake -aI..\Specs -aO..\Lib -aL..\Lib -L..\Bin example1
and
gnatprep -DPredicates_Here=False example.adb example2.adb
gnatmake -aI..\Specs -aO..\Lib -aL..\Lib -L..\Bin example2
gnatmake -c -aI..\Specs -aL..\Lib Dinner_Services
gnatdll -d Dinner.dll -e ..\Bin\AdaLSX.def -I..\Lib -I..\Bin Dinner_Services.ali -n
and then run as example1.exe and example2.exe (note, that the command file build.bat in the directory Samples builds all three configurations -- that is, the two just discussed, and that with Prolog main program, which was discussed before).