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:
tk_new/2
tcl_eval/3
tk_main_loop
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 ***" } }
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.