Explanatory examples - janus-py
Using
janus-py
Although Python and Prolog have similarities at the
data structure level (Section 1.2) they
differ substantially in their execution. In terms of
input, janus-py
functions are either
Variadic: passing to XSB a module, a predicate name, zero or more input arguments and zero or more keyword arguments (
jns.apply_once()
,jns.apply()
andjns.comp()
); orString-based: passing a Prolog goal as a string, with input and output bindings passed via dictionaries (
jns.query_once()
andjns.query()
).
In terms of output, janus-py
functions
have three different behaviors.
Deterministic: passing back a single answer (
jns.apply_once()
,jns.query_once())
;Itertor-based: returning answers for a Prolog goal G via an instance of a class whose iterator backtracks through answers to G (
jns.apply()
,jns.query())
; orComprehension-based: passing back multiple answers as a list or set (
jns.comp())
.
We discuss these various approaches using a series of examples.
Deterministic Queries and Commands
In these examples, features of janus-py
are presented via commands and deterministic queries
before turning to general support of non-deterministic
queries. We begin with the variadic deterministic calls
(jns.apply_once()
and jns.cmd
);
and then proceed to the deterministic string-based call
jns.query_once()
.
Variadic Deterministic Queries and Commands
Example 2.1. Calling a
deterministic query via jns.apply_once()
*
*
As described in Section [jns-py:config]
janus
is loaded like any other Python module.
Once loaded, a simple way to use janus
is to
execute a deterministic query to XSB. The Python
statement:
>>> Ans = jns.apply_once('basics','reverse',[1,2,3,('mytuple'),{'a':{'b':'c'}}])
asks XSB to reverse a list using
basics:reverse(+,-)
– i.e., with the first
argument ground and the second argument free. To execute
this query the input list along with the tuple and
dictionary it contains are translated to XSB terms as
described in Section 1.2, the query
is executed, and the answer translated back to Python and
assigning Ans
the value
[{'a':{'b':'c'}},('mytuple'),3,2,1]
For learning janus
or for tutorials, a
family of pretty printing calls can be useful.
Example 2.2. Viewing
janus-py
in Prolog Syntax
The pp_jns_apply_once()
function calls
jns_apply_once()
and upon return pretty
prints both the call and return in a style like that used
in XSB”s command line interface. For example if the
following call is made on the Python command line
interface:
>>> pp_jns_apply_once('basics','reverse',[1,2,3,('mytuple'),{'a':{'b':'c'}}])
the function will print out both the query and answer in Prolog syntax as if it were executed in XSB.[^19]
?- basics:reverse(([1,2,3,-(mytuple), {a:{b:c}}],Answer).
Answer = [{a:{b:c}}, mytuple, 3, 2, 1]
TV = True
Note that the Python calls in the above example each
had a module name as their first position, a function name
in their second position, and the Prolog query argument in
their third position. The translation to XSB by
jns.apply_once()
adds an extra unbound
variable as the last argument in the query so that the
query had two arguments.
The variadic jns.cmd()
provides a
convenient way manage the Prolog session from Python.
Example 2.3. **Session management in
janus-py
using jns.cmd()
* *
Once janus-py
has been imported
(initializing XSB), any user XSB code can be loaded
easily. One can execute
>>> jns.cmd('consult','consult','xsb_file')
which loads the XSB file
xsb_file.{P,pl}
, compiling it if necessary.
Note that unlike (the default behavior of)
jns_apply_once()
, jns.cmd()
does
not add an extra return argument to the Prolog call. For
convenience and compatibility, janus-py
also
defines a shortcut for consulting:
>>> jns.consult('xsb_file')
janus-py
also provides shortcuts for
some other frequent Prolog calls – other desired shortcuts
are easily implemented via Python functions.
If a Prolog file xsb_file.P
, is
modified it can be reconsulted in the same session just as
if the XSB interpreter were being used. Indeed, using
janus-py
, the Python interpreter can be used
as a command-line interface for writing and debugging XSB
code (although the XSB interpreter is recommended for most
XSB development tasks).
The following example shows how Python can handle errors thrown by Prolog.
Example 2.4. **Error handling in
janus-py
* *
If an exception occurs when XSB is executing a
goal, the error can be caught in XSB by
catch/3
in the usual manner. If the error is
not caught by user code, it will be caught by
janus-py
, translated to a Python exception of
the vanilla Exception
class,[^20] and can be
caught as any other Python exception of that type. Precise
information about the XSB exception is available to Python
through the janus-py
function
jns_get_error_message()
,
Consider what happens when trying to consult a file
that doesn’t exist in any of the XSB library paths. In
this case, XSB’s consult/1
throws an
exception, the janus-py
sub-system catches it
and raises a Python error as mentioned above. The Python
error is easily handled: for instance by calling the
function in a block such as the following:
try
<some jns.function>
except Exception as err:
display_xsb_error(err)
where display_xsb_error()
is a call to
the function:
def display_xsb_error(err):
print('Exception Caught from XSB: ')
print(' ' + jns.get_error_message())
where, jns.get_error_message()
is
calls C to return the last janus-py
error
text as a string. If an exception arises during execution
of a janus-py
function the function returns
the value None
in addition to setting a
Python Error.
Error handling is performed automatically in
pp_jns_apply_once()
and other pretty-printing
calls.
Although the string-based queries are the most general
way for Python to query Prolog, the variadic functions
jns.apply_once()
and jns.cmd()
and jns.comp()
(to be introduced) can all
make queries with different numbers of input
arguments.
Example 2.5. **Varying the number of
arguments in jns.apply_once()
and
jns.cmd()
* *
Suppose you wanted to make a ground Prolog query,
say ?- p(a)
: the information answered by this
query would simply indicate whether the atom
p(a)
was true
,
false
,,or undefined
in the
Well-Founded Model. In janus-py
such a query
could most easily be made via the janus-py
function jns.cmd()
>>> jns.cmd('jns_test','p','a')
Since jns.cmd
does not return any
answer bindings, it returns the truth value directly to
Python, rather than as part of a tuple. However,
jns.apply_once()
and jns.cmd()
are both variadic functions so that the number of input
arguments can also vary as shown in the table
below.
jns.cmd(’mod’,’cmd’) |
calls the goal | mod:cmd() |
jns.cmd(’mod’,’cmd’,’a’) |
calls the goal | mod:cmd(a) |
jns.cmd(’mod’,’cmd’,’a’,’b’) |
calls the goal | mod:cmd(a,b) |
jns.apply_once(’mod’,’pred’) |
calls the goal | mod:pred(X1) |
jns.apply_once(’mod’,’pred’,’a’) |
calls the goal | mod:pred(a,X1) |
More generality is allowed in the non-deterministic
jns.comp()
discussed more fully in Section 2.2.2.3. In
jns.comp()
the optional keyword argument
vars
can be used to indicate the number of
return arguments desired. So, if vars=2
were
added as a keyword argument, two arguments arguments would
be added to the call, with each a free variable. Combining
both approaches, a variety of different Prolog queries can
be made as shown in the following table. [^21]
jns.comp(’mod’,’pred’),vars=2 |
calls the goal | mod:pred(X1,X2) |
jns.comp(’mod’,’pred’,’a’,vars=0) |
calls the goal | mod:pred(a) . |
jns.comp(’mod’,’pred’,’a’,vars=1) |
calls the goal | mod:pred(a,X1) |
jns.comp(’mod’,’pred’,’a’,vars=2) |
calls the goal | mod:pred(a,X1,X2) |
jns.comp(’mod’,’pred’,’a’,’b’,vars=2) |
calls the goal | mod:pred(a,b,X1,X2) |
Deterministic String Queries
A more general approach to querying Prolog is to use
one of the string-based functions – either the
deterministic jns.query_once()
or the
non-deterministic jns.query()
. These
functions support logical variables so that each argument
of the call can be ground, uninstantiated or partially
ground. To support this generality, a slightly more
sophisticated setup is required, and the invocations are
somewhat slower. (See Section 2.4 for timings.)
Example 2.6. **Calling a deterministic
query via jns.query_once()
The Prolog goal in Example 2.1 can also be
executed using jns.query_once()
by forming a
syntactically correct Prolog query and specifying the
bindings that are required for Prolog variables. For
instance, a function call such as the following could be
made:
AnsDict = jns.query_once('basics:reverse(List,RevList)',
inputs={'List'=[1,2,3,('mytuple'),{'a':{'b':'c'}}]})
Note that both List
and
RevList
are treated as logical variables by
Prolog. When the function is called the value of the index
’List’
in the dictionary inputs
will be translated to Prolog syntax:
[1,2,3,-(mytuple),{a:{b:c}}]
(which has
nearly the same syntax as the corresponding Python data
structure). This Prolog term will be unified with the
logical variable List
so that the following
Prolog goal is called:
?- basics:reverse([1,2,3,-(mytuple),{a:{b:c}}],RevList)
upon return assigning to Answer
the
Python return dictionary**
{ 'RevList':{'a':{'b':'c'}},('mytuple'),3,2,1], truth:True }
in which the logical variable name
’RevList’
is a key of the return dictionary.
Note that the return dictionary contains not only all
bindings to all logical variables in the query, but also a
truth value. In this case
>>> AnsDict['truth'] = True
By default, jns.query_once()
,
jns.query()
, and jns.com()
,
return one of three truth values
True
representing the truth value true. This means the XSB query succeeded and that the answer with bindings (AnsDict[’RevList’]
) is true in the Well-Founded Model of the program.[^22]False
representing the truth value false. This means the XSB query failed and has no answers in the Well-Founded Model of the program. In such a case, the return dictionary has the form{truth:False}
jns.Undefined
representing the truth value undefined. This means that the XSB query succeeded, but the answer is neither true nor false* in the Well-Funded Model of the program.[^23]*
Although XSB’s three-valued logic can be highly useful for many purposes, it can be safely ignored in many applications, and most queries will either succeed with true answers or will fail.[^24]
Although the above call of
jns.query_once()
uses a single input and
output variable, jns.query_once()
is in fact
highly flexible. Once could alternately call the goal with
the input variable already bound:
Answer = jns.query_once("basics:reverse([1,2,3,{'a':{'b':'c'}}],Rev)")
which would produce the same return dictionary as before.
One can even call
Answer = jns.query_once('basics:reverse([1,2,3,-('mytuple'),{'a':{'b':'c'}}],
{'a':{'b':'c'}},-('mytuple'),3,2,1])'
which would produce the return dictionary
{’truth’:True}
. It should be noted that any
data structures within the goal string (i.e., the second
argument of jns.query_once()
) must already be
in Prolog syntax, so it easier to use logical variables
and dictionaries for input and output whenever the Python
and Prolog syntaxes diverge (e.g., for tuples and
sets).
One also can use more than one input variable: for instance the call
>>> Answer = jns.query_once('basics:reverse([1,2,3,InputTuple,InputDict],RetList)',
inputs={InputTuple:-('mytuple'),InputDict={'a':{'b':'c'}}])
which is equivalent to the Prolog query:
?- InputTuple:-(mytuple),InputDict={a:{b:c}},
basics:reverse([1,2,3,InputTuple,InputDict],RetList),
which would produce a return dictionary with the
binding to RetList
as above.
Non-Deterministic Queries
There are three ways to call non-deterministic Prolog
queries in janus-py
. A class – either the
variadic jns.apply
or the string-based
jns.query
– can be instantiated whose
iterator backtracks through Prolog answers. Alternately,
the Prolog answers can be comprehended into a list or set
and returned to Python. We consider each of these cases in
turn.
Variadic Non-Deterministic Queries
Consider the predicate test_nd/2
in the
Prolog module jns_test
.
test_nd(a,1).
test_nd(a,11).
test_nd(a,111).
test_nd(b,2).
test_nd(c,3).
test_nd(d,4).
test_nd(d,5):- unk(something),p.
test_nd(d,5):- q,unk(something_else).
test_nd(d,5):- failing_goal,unk(something_else).
p.
q.
In this module, the predicate unk/1
is
defined as
unk(X):- tnot(unk(X)).
so that for a ground input term T unk(T)
succeeds with the truth value undefined in the
program’s Well-Founded Model. The call
jns.apply('jns_test','test_nd','a')
creates an instance of the Python class
jns.apply
that can be used to backtrack
through the answers to test_nd(e,X)
. Such a
class can be used wherever a Python iterator object can be
used, for instance;
C1 = jns.apply('jns_test','test_nd','a')
for answer in C1:
...
will iterate through all answers to the Prolog goal
test_nd(X,Y)
.
String-Based Non-Deterministic Queries
String-based non-deterministic queries are similar to
jns.apply()
. For the program
jns_test
of Section 2.2.2.1 the goal
jns.query('jns_test','test_nd(X,Y)')
creates an instance of the Python class
jns.query
that can be used to iterate through
solutions to the Prolog goal e.g.,
C1 = jns.query('jns_test','test_nd(X,Y)')
for answer in C1:
...
The handling of input and output variable bindings is
exactly as in jns.query_once
in Section 2.2.2.
The next example shows different ways in which
janus-py
can express truth values.
Example 2.7. Expressing Truth Values
In jns.query_once()
,
jns.query()
and jns.comp()
truth
values can be expressed in different ways. Consider the
fragment:
for ans in jns.query('jns_test','test_nd(d,Y)')
print(ans)
would print out by default
{d:4, truth:True}
{d:5, truth:Undefined}
{d:5, truth:Undefined}
While this default behavior is the best choice for
most purposes, there are cases where the delay list of
answers needs to be accessed (cf. Volume 1, chapter 5) for
instance if the answers are to be displayed in a UI or
sent to an ASP solver. In such a case, the keyword
argument truth_vals
can be set to
DELAY_LISTS
, so that the fragment
for ans in jns.query('jns_test','test_nd(d,Y)',truth_vals=DELAY_LISTS)
print(ans)
prints out
{Y:4, DelayList:[]}
{Y:5, DelayList:(plgTerm, unk, something)]}}
{Y:5, DelayList:(plgTerm, unk, something_else)]}
In XSB’s SLG resolution, a delay list is a set of Prolog literals, but Prolog literals cannot be directly represented in Python. XSB addresses this by serializing a term T as follows:
foooof̄oooof̄oooof̄oooof̄oooooooooooooooooooooōoō if
T is a non-list
term T = f(arg1, ..., argn):
serialize(T) = (plgterm,serialize(rg1), ..., serialize(argn))
else serialize(T) = T
so that the delay list received by Python is a list of serialized literals.
Alternately, if one were certain that no answers
would have the truth value undefined, the keyword
argument truth_vals
could be set to
NO_TRUTHVALS
. For instance
for ans in jns.query('jns_test','test_nd(a,Y)',truth_vals=NO_TRUTHVALS)
print(ans)
prints out
{Y:1}
{Y:11}
{Y:111}
Comprehension of Non-Deterministic Queries
The handling of set and list comprehension in
janus
is likely either to undergo a major
revision or to become obsolescent.
A declarative aspect of Python is its support for
comprehension of lists, sets and aggregates.
janus-py
can fit non-deterministic queries
into this paradigm with query comprehension:
calls to XSB that return all solutions to an XSB query as
a Python list or all unique solutions as a set.
Example 2.8. **List and Set
Comprehension in janus-py
* *
Consider again the program jns_test
introduced in Section 2.2.2.1. The Python
function
>>> jns.comp('jns_test','test_comp',vars=2)
calls the XSB goal
?- jns_test:test_comp(X1,X2)
in a manner
somewhat similar to jns.apply_once()
in
Section 2.2.2.1, but with several
important differences. First, the keyword argument
vars
set to 2
means that there
are two return variables. Another difference is that the
call to jns.comp()
returns multiple solutions
as a list of tuples, rather than using an iterator to
return a sequence of answer dictionaries. Formatted this
return is:
[
((e,5),2),((e,5),2),
((d,4),1),((c,3),1),
((b,2),1),((a,1),1)
((a,11),1),((a,111),1)
]
Note that each answer in the comprehension is a
2-ary tuple, the first argument of which represents
bindings to the return variables, and the second its truth
value: true* as 1
, undefined as
2
.*
>>> jns.comp('jns_test','test_comp',vars=2,set_collect=True)
returns a set rather than a list.[^25] If there are
no answers that satisfy the query jns.comp()
returns the empty list or set.
Whether it is a list or a set, the return of
jns.comp()
will be iterable and can be used
as any other object of its type, for example:
>>> for answer,tv in jns.comp('jns_test','test_comp',vars=2):
... print('answer = '+str(answer)+' ; tv = '+str(tv))
...
answer = ('e', 5) ; tv = 2
answer = ('e', 5) ; tv = 2
answer = ('d', 4) ; tv = 1
answer = ('c', 3) ; tv = 1
answer = ('b', 2) ; tv = 1
answer = ('a', 1) ; tv = 1
answer = ('a', 11) ; tv = 1
answer = ('a', 111) ; tv = 1
As with jns.query()
,
jns.comp()
also supports the different
options for the keyword argument truth_vals
(cf. Section 2.2.2.2).
Callbacks from XSB to Python
When XSB is called from Python, janus
can
easily be used to make callbacks to Python. For instance,
the query:
jns.apply_once('jns_callbacks','test_json')
calls the XSB goal
jns_callbacks:test_json(X)
as usual. The file
jns_callbacks.P
can be found in the
directory
$XSB_ROOT/xsbtests/janus_tests
This directory contains many other examples including
those discussed in this chapter. In particular,
jns_callbacks.P
contains the predicate:
test_json(Ret):-
pyfunc(xp_json,
prolog_loads('{"name":"Bob","languages": ["English","Fench","GERMAN"]}'),
Ret).
that calls the Python JSON loader as in Example 1.1, returning the tuple
({'name': 'Bob', 'languages': ['English', 'Fench', 'GERMAN']}, 1)
to Python. This example shows how easy it can be for
XSB and Python to work together: the Python call
jns.apply_once('jns_callbacks','test_json')
causes the JSON structure to be read from a string into a
Python dictionary, translated to a Prolog dictionary term,
and then back to a Python dictionary associated with its
truth value.
As another example of callbacks consider the goal:
TV = jns.apply_once('jns_callbacks','test_class','joe')
that calls the XSB rule:
test_class(Name,Obj):-
jns.apply_once('jns_callbacks','test_class','joe')
that in turn creates an instance of the class
Person
via:
class Person:
def __init__(self, name, age, favorite_ice_cream=None):
self.name = name
self.age = age
if favorite_ice_cream is None:
favorite_ice_cream = 'chocolate'
self.favorite_ice_cream = favorite_ice_cream
The reference to the new Person
instance
is returned to Prolog, then back to Python and assigned to
the variable NewClass
. Afterwards, accessing
NewClass
properties from the original Python
command line:
>>> NewClass.name
is evaluated to ’joe’
as expected. In
other words, the Python environment calling XSB and that
called by XSB are one and the same. The coupling between
Python and XSB is both implementationally tight and
semantically transparent; micro-computations can be
shifted between XSB and Python depending on which language
is more useful for a given purpose.
Constraints
The material in this section is not necessary to
understand for a basic use of janus-py
, but
shows how janus-py
can be used for
constraint-based reasoning.
Example 2.9. **Evaluating queries with constraints* *
XSB provides support for constraint-based reasoning
via CLP(R) [@Holz95] both for Prolog-style
(non-tabled) and for tabled resolution. However, using
constraint-based reasoners like CLP(R) requires explicit
use of logical variables (cf. Chapter [chap:constraints]),
and as mentioned in Section 2.2.1,
janus-py
does not provide a direct way to
represent logical variables since logical variables do not
naturally correspond to a Python type. Fortunately, it is
not difficult to pass constraint expressions containing
logical variables to XSB within Python strings.
Consider a query to find whether
X > 3 * Y + 2, Y > 0 ⊨ X > Y
In CLP(R) this is done by writing a clause to
assert the two constraints – in Prolog syntax as calls to
the literals {X
>3*Y + 2}
and
{Y
>0}
– and then
calling the CLP(R) goal entailed(Y
>0)
. Within
XSB, one way to generalize the entailment relation into a
predicate would be to see if one set of constraints,
represented as a list, implies a given
constraint:
:- import {}/1,entailed/1 from clpr.
check_entailed(Constraints,Entailed):-
set_up_constraints(Constraints),
entailed(Entailed).
set_up_constraints([]).
set_up_constraints([Constr,Rest]):-
{Constr},
set_up_constraints(Rest).
Using our example, a query to this predicate would have the form
?- check_entailed([X > 3*Y + 2,Y>0],X > Y).
This formulation requires the logical variables
X
and Y
to be passed into the
call. Checking constraint entailment via
janus-py
only requires writing the
constraints as a string in Python and later having XSB
read the string. A predicate to do this from Python is
similar to check_entailed/2
above, but
unpacks the constraints from the input atom (i.e., the XSB
translation of the Python string).
:- import read_atom_to_term/3 from string.
jns.check_entailed(Atom):-
read_atom_to_term(Atom,Cterm,_Cvars),
Cterm = [Constraints,Entailed],
set_up_constraints(Constraints),
entailed(Entailed).
The function call from Python is simply:
>>> jns.cmd('jns_constraints','check_entailed','[[X > 3*Y + 2,Y>0],[X > Y]]')
Note that the only difference when calling from
Python is to put the two arguments together into a single
Python string, so that XSB’s reader treats the
Y
variable in the input constraints and the
entailment constraint as the same [^26]
Other
janus-py
Resources and Examples
Many of the examples from this chapter have been saved
into Jupyter notebooks in
$XSB_ROOT/XSB/examples
, with associated PDF
files in $XSB_ROOT/docs/JupyterNotebooks
.
In addition, as mentioned earlier the directory
$XSB_ROOT/xsbtests/janus_tests
contains a
series of tests for most of the examples in this chapter
and many others.