Tcl The Master, Prolog The Slave

This is the classical way that GUIs are bolted onto applications. The slave (in this case Prolog) sits mostly idle while the user interacts with the GUI, for example filling in a form. When some action happens in the GUI that requires information from the slave (a form submit, for example), the slave is called, performs a calculation, and the GUI retrieves the result and updates its display accordingly.

In our Prolog+Tcl/Tk setting this involves the following steps:

Some of The Tk widgets in the GUI will have "callbacks" to Prolog, i.e. they will call the prolog Tcl command. When the Prolog call returns, the values stored in the prolog_variables array in the Tcl interpreter can then be used by Tcl to update the display.

Here is a simple example of a callback. The Prolog part is this:

     :- use_module(library(tcltk)).
     
     hello('world').
     
     go :-
         tk_new([], Tcl),
         tcl_eval(Tcl, 'source simple.tcl', _),
         tk_main_loop.
     

which just loads the library(tcltk), defines a hello/1 data clause, and go/0, which starts a new Tcl/Tk interpreter, loads the code simple.tcl into it, and passes control to Tcl/Tk.

The Tcl part, simple.tcl, is this:

     label .l -textvariable tvar
     button .b -text "push me" -command { call_and_display }
     pack .l .b -side top
     
     proc call_and_display { } {
         global tvar
     
         prolog "hello(X)"
         set tvar $prolog_variables(X)
     }
     

which creates a label, with an associated text variable, and a button, that has a call back procedure, call_and_display, attached to it. When the button is pressed, call_and_display is executed, which simply evaluates the goal hello(X) in Prolog and the text variable of the label .l to whatever X becomes bound to, which happens to be world. In short, pressing the button causes the word world to be displayed in the label.

Having Tcl as the master and Prolog as the slave, although a simple model to understand and implement, does have disadvantages. The Tcl command prolog is determinate, i.e. it can return only one result with no backtracking. If more than one result is needed it means either performing some kind of all-solutions search and returning a list of results for Tcl to process, or asserting a clause into the Prolog clause store reflecting the state of the computation.

Here is an example of how an all-solutions search can be done. It is a program that calculates the outcome of certain ancestor relationships; i.e. enter the name of a person, click on a button and it will tell you the mother, father, parents or ancestors of that person.

The Prolog portion looks like this (see also library('tcltk/examples/ancestors.pl')):

     :- use_module(library(tcltk)).
     
     go :- tk_new([name('ancestors')], X),
         tcl_eval(X, 'source ancestors.tcl', _),
         tk_main_loop,
         tcl_delete(X).
     
     father(ann, fred).
     father(fred, jim).
     mother(ann, lynn).
     mother(fred, lucy).
     father(jim, sam).
     
     parent(X, Y) :- mother(X, Y).
     parent(X, Y) :- father(X, Y).
     
     ancestor(X, Y) :- parent(X, Y).
     ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
     
     all_ancestors(X, Z) :- findall(Y, ancestor(X, Y), Z).
     
     all_parents(X, Z) :-   findall(Y, parent(X, Y), Z).
     

This program consists of three parts. The first part is defined by go/0, the now familiar way in which a Prolog program can create a Tcl/Tk interpreter, load a Tcl file into that interpreter, and pass control over to the interpreter.

The second part is a small database of mother/father relationships between certain people through the clauses mother/2 and father/2.

The third part is a set of "rules" for determining certain relationships between people: parent/2, ancestor/2, all_ancestors/2 and all_parents/2.

The Tcl part looks like this (see also library('tcltk/examples/ancestors.tcl')):

                             
% ancestors.pl
#!/usr/bin/wish # set up the tk display # construct text filler labels label .search_for -text "SEARCHING FOR THE" -anchor w label .of -text "OF" -anchor w label .gives -text "GIVES" -anchor w # construct frame to hold buttons frame .button_frame # construct radio button group radiobutton .mother -text mother -variable type -value mother radiobutton .father -text father -variable type -value father radiobutton .parents -text parents -variable type -value parents radiobutton .ancestors -text ancestors -variable type -value ancestors # add behaviors to radio buttons .mother config -command { one_solution mother $name} .father config -command { one_solution father $name} .parents config -command { all_solutions all_parents $name} .ancestors config -command { all_solutions all_ancestors $name} # create entry box and result display widgets entry .name -textvariable name label .result -text ">>> result <<<" -relief sunken -anchor nw -justify left # pack buttons into button frame pack .mother .father .parents .ancestors -fill x -side left -in .button_frame # pack everything together into the main window pack .search_for .button_frame .of .name .gives .result -side top -fill x # now everything is set up
% ancestors.pl
# defined the callback procedures # called for one solution results proc one_solution { type name } { if [prolog "${type}('$name', R)"] { display_result $prolog_variables(R) } else { display_result "" } } # called for all solution results proc all_solutions { type name } { prolog "${type}('$name', R)" display_result $prolog_variables(R) } # display the result of the search in the results box proc display_result { result } { if { $result != "" } { # create a multiline result .result config -text $result } else { .result config -text "*** no result ***" } }

images/tcltkancestors.png

Ancestors Calculator
This program is in two parts. The first part sets up the Tk display, which consists of four radiobuttons to choose the kind of relationship we want to calculate, an entry box to put the name of the person we want to calculate the relationship over, and a label in which to display the result.

Each radio buttons has an associated callback. Clicking on the radio button will invoke the appropriate callback, apply the appropriate relationship to the name entered in the text entry box, and display the result in the results label.

The second part consists of the callback procedures themselves. There are actually just two of them: one for a single solution calculation, and one for an all-solutions calculation. The single solution callback is used when we want to know the mother or father as we know that a person can have only one of each. The all-solutions callback is used when we want to know the parents or ancestors as we know that these can return more than one results. (We could have used the all-solutions callback for the single solutions cases too, but we would like to illustrate the difference in the two approaches.) There is little difference between the two approaches, except that in the single solution callback, it is possible that the call to Prolog will fail, so we wrap it in an if ... else construct to catch this case. An all-solutions search, however, cannot fail, and so the if ... else is not needed.

But there are some technical problems too with this approach. During a callback Tk events are not serviced until the callback returns. For Prolog callbacks that take a very short time to complete this is not a problem, but in other cases, for example during a long search call when the callback takes a significant time to complete, this can cause problems. Imagine that, in our example, we had a vast database describing the parent relationships of millions of people. Performing an all-solutions ancestors search could take a long time. The classic problem is that the GUI no longer reacts to the user until the callback completes.

The solution to this is to sprinkle tk_do_one_event/[0,1] calls throughout the critical parts of the Prolog code, to keep various kinds of Tk events serviced.

If this method is used in its purest form, then it is recommended that after initialization and passing of control to Tcl, Prolog do not make calls to Tcl through tcl_eval/3. This is to avoid programming spaghetti. In the pure master/slave relationship it is a general principle that the master only call the slave, and not the other way around.