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()
and
jns.comp()
); or
String-based: passing a Prolog goal as a string,
with input and output bindings passed via dictionaries
(jns.query_once()
and
jns.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())
; or
Comprehension-based: passing back multiple answers
as a list or set (jns.comp())
.
We discuss these various approaches using a series of examples.
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()
.
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) |
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.
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.
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 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}
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).
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.
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]
janus-py
Resources and ExamplesMany 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.