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() 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.

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.



The Prolog Implementers Forum is a part of the "All Things Prolog" online Prolog community, an initiative of the Association for Logic Programming stemming from the Year of Prolog activities.