Did you know ... | Search Documentation: |
Calling Prolog from Python |
The Janus interface can also call Prolog from Python. Calling Prolog
from Python is the basis when embedding Prolog into Python using the
Python package janus_swi
. However, calling Prolog from
Python is also used to handle call backs. Mutually recursive
calls between Python and Prolog are supported. They should be handled
with some care as it is easy to crash the process due to a stack
overflow.
Loading janus into Python is realized using the Python package
janus-swi
, which defines the module janus_swi
.
We do not call this simply janus
to allow coexistence of
Janus for multiple Prolog implementations. Unless you plan to interact
with multiple Prolog systems in the same session, we advise importing
janus for SWI-Prolog as below.
import janus_swi as janus
If Python is embedded into SWI-Prolog, the Python module may be
imported both as janus
and janus_swi
. Using
janus
allows the same Python code to be used from different
Prolog systems, while using janus_swi
allows the same code
to be used both for embedding Python into Prolog and Prolog into Python.
In the remainder of this section we assume the Janus functions are
available in the name space janus
.
The Python module janus
provides utility functions and
defines the classes janus.query(), janus.apply(),
janus.Term(), janus.Undefined()
and
janus.PrologError().
The Python calling Prolog interface consist of four primitives, distinguishing deterministic vs. non-deterministic Prolog queries and two different calling conventions which we name functional notation and relational notation. The relational calling convention specifies a Prolog query as a string with an input dict that provides (input) bindings for part of the variables in the query string. The results are represented as a dict that holds the bindings of the output variables and the truth value (see section 5.4). For example:
>>> janus.query_once("Y is sqrt(X)", {'X':2}) {'truth': True, 'Y': 1.4142135623730951}
The functional notation calling convention specifies the query as a module, predicate name and input arguments. It calls the predicate with one argument more than the number of input arguments and translates the binding of the output argument to Python. For example
>>> janus.apply_once("user", "plus", 1, 2) 3
The table below summarizes the four primitives.
Relational notation | Functional notation | |
det | janus.query_once() | janus.apply_once() |
nondet | janus.query() | janus.apply() |
We start our discussion by introducing the janus.query_once(query,inputs) function for calling Prolog goals as once/1. A Prolog goal is constructed from a string and a dict with input bindings and returns output bindings as a dict. For example
>>> import janus_swi as janus >>> janus.query_once("Y is X+1", {"X":1}) {'Y': 2, 'truth': True}
Note that the input argument may also be passed literally. Below we give two examples. We strongly advise against using string interpolation for three reasons. Firstly, the query strings are compiled and cached on the Prolog sided and (thus) we assume a finite number of distinct query strings. Secondly, string interpolation is sensitive to injection attacks. Notably inserting quoted strings can easily be misused to create malicious queries. Thirdly and finally, serializing and deserializing the data is generally slower then using the input dictionary, especially if the data is large. Using a dict for input and output together with a (short) string to denote the goal is easy to use and fast.
>>> janus.query_once("Y is 1+1", {}) # Ok for "static" queries {'Y': 2, 'truth': True} >>> x = 1 >>> janus.query_once(f"Y is {x}+1", {}) # WRONG, See above {'Y': 2, 'truth': True}
The output dict contains all named Prolog variables that (1) are not in the input dict and (2) do not start with an underscore. For example, to get the grandparents of a person given parent/2 relations we can use the code below, where the _GP and _P do not appear in the output dict. This both saves time and avoids the need to convert Prolog data structures that cannot be represented in Python such as variables or arbitrary compound terms.
>>> janus.query_once("findall(_GP, parent(Me, _P), parent(_P, _GP), GPs)", {'Me':'Jan'})["GPs"] [ 'Kees', 'Jan' ]
In addition to the variable bindings the dict contains a key
truth
5Note that
variable bindings always start with an uppercase latter.
that represents the truth value of evaluating the query. In normal
Prolog this is a Python Boolean. In systems that implement Well
Founded Semantics, this may also be an instance of the class janus.Undefined().
See
section 5.4 for details. If
evaluation of the query failed, all variable bindings are bound to the
Python constant None
and the truth
key has the
value False
. The following Python function returns True
if the Prolog system supports unbounded integers and False
otherwise.
def hasBigIntegers(): janus.query_once("current_prolog_flag(bounded,false)")['truth']
While janus.query_once()
deals with semi-deterministic goals, the class janus.query()
implements a Python
iterator that iterates over the solutions of a Prolog goal. The
iterator may be aborted using the Python break
statement.
As with janus.query_once(),
the returned dict contains a truth
field. This field cannot
be False
though and thus is either True
or an
instance of the class
janus.Undefined()
import janus_swi as janus def printRange(fr, to): for d in janus.query("between(F,T,X)", {"F":fr, "T":to}): print(d["X"])
The call to janus.query() returns an object that implements
both the iterator protocol and the context manager protocol. A context
manager ensures that the query is cleaned up as soon as it goes out of
scope - Python typically does this with for loops, but there is no
guarantee of when cleanup happens, especially if there is an error. (You
can think of a with
statement as similar to Prolog's setup_call_cleanup/3.)
Using a context manager, we can write
def printRange(fr, to): with janus.query("between(F,T,X)", {"F":fr, "T":to}) as d_q: for d in d_q: print(d["X"])
Iterators may be nested. For example, we can create a list of tuples like below.
def double_iter(w,h): tuples=[] for yd in janus.query("between(1,M,Y)", {"M":h}): for xd in janus.query("between(1,M,X)", {"M":w}): tuples.append((xd['X'],yd['Y'])) return tuples
or, using context managers:
def doc_double_iter(w,h): tuples=[] with janus.query("between(1,M,Y)", {"M":h}) as yd_q: for yd in yd_q: with janus.query("between(1,M,X)", {"M":w}) as xd_q: for xd in xd_q: tuples.append((xd['X'],yd['Y'])) return tuples
After this, we may run
>>> demo.double_iter(2,3) [(1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3)]
In addition to the iterator protocol that class janus.query() implements, it also implements the methods janus.query.next() and janus.query.close(). This allows for e.g.
q = query("between(1,3,X)") while ( s := q.next() ): print(s['X']) q.close()
or
try: q = query("between(1,3,X)") while ( s := q.next() ): print(s['X']) finally: q.close()
The close() is called by the context manager, so the following is equivalent:
with query("between(1,3,X)") as q: while ( s := q.next() ): print(s['X'])
But, iterators based on Prolog goals are fragile. This is because, while it is possible to open and run a new query while there is an open query, the inner query must be closed before we can ask for the next solution of the outer query. We illustrate this using the sequence below.
>>> q1 = query("between(1,3,X)") >>> q2 = query("between(1,3,X)") >>> q2.next() {'truth': True, 'X': 1} >>> q1.next() Traceback (most recent call last): ... swipl.Error: swipl.next_solution(): not inner query >>> q2.close() >>> q1.next() {'truth': True, 'X': 1} >>> q1.close()
Failure to close a query typically leaves SWI-Prolog in an inconsistent state and further interaction with Prolog is likely to crash the process. Future versions may improve on that. To avoid this, it is recommended that you use the query with a context manager, that is using the Python constwith statement.
True
, such
changes are preserved.
>>> query_once("b_setval(a, 1)", keep=True) {'truth': 'True'} >>> query_once("b_getval(a, X)") {'truth': 'True', 'X': 1}
If query fails, the variables of the query are bound to
the Python constant None
. The bindings object
includes a key
truth
6As this name is
not a valid Prolog variable name, this cannot be ambiguous.
that has the value False
(query failed, all bindings are None
), True
(query succeeded, variables are bound to the result converting Prolog
data to Python) or an instance of the class janus.Undefined().
The information carried by this instance is determined by the truth
parameter. Below is an example. See section
5.4 for details.
>>> import janus_swi as janus >>> janus.query_once("undefined") {'truth': Undefined}
See also janus.cmd() and janus.apply_once(), which provide a fast but more limited alternative for making ground queries (janus.cmd()) or queries with leading ground arguments followed by a single output variable.
module:predicate(Input ... , Output)
,
where
Input are the Python input arguments converted to
Prolog. On success, Output is converted to Python and
returned. On failure a janus.PrologError()
exception is raised unless the fail
parameter is specified.
In the latter case the function returns obj. This interface
provides a comfortable and fast calling convention for calling a simple
predicate with suitable calling conventions. The example below returns
the home directory of the SWI-Prolog installation.
>>> import janus_swi as janus >>> janus.apply_once("user", "current_prolog_flag", "home") '/home/janw/src/swipl-devel/build.pdf/home'
truth
key in janus.query_once().
For example:
>>> import janus_swi as janus >>> cmd("user", "true") True >>> cmd("user", "current_prolog_flag", "bounded", "true") False >>> cmd("user", "undefined") Undefined >>> cmd("user", "no_such_predicate") Traceback (most recent call last): File "/usr/lib/python3.10/code.py", line 90, in runcode exec(code, self.locals) File "<console>", line 1, in <module> janus.PrologError: '$c_call_prolog'/0: Unknown procedure: no_such_predicate/0
The function janus.query_once() is more flexible and provides all functionality of janus.cmd(). However, this function is faster and in some scenarios easier to use.
None
and the text is read from file. If data
is a string, it provides the Prolog text that is loaded and file
is used as identifier for source locations and error messages.
The module argument denotes the target module. That is where
the clauses are added to if the Prolog text does not define a module or
where the exported predicates of the module are imported into.
If data is not provided and file is not accessible this raises a Prolog exception. Errors that occur during the compilation are printed using print_message/2 and can currently not be captured easily. The script below prints the train connections as a list of Python tuples.
import janus_swi as janus janus.consult("trains", """ train('Amsterdam', 'Haarlem'). train('Amsterdam', 'Schiphol'). """) print([d['Tuple'] for d in janus.query("train(_From,_To),Tuple=_From-_To")])
data
and module
keyword arguments are
SWI-Prolog extensions.
Class janus.query() is similar to the janus.query_once() function, but it returns a Python iterator that allows for iterating over the answers to a non-deterministic Prolog predicate.
The iterator also implements the Python context manaager protocol
(for the Python with
statement).
truth
False
.
See discussion above.
keep
is a SWI-Prolog extension.|
None janus.query.next()query
as an iterator is to be preferred. See discussion
above. q.next()
is equivalent to next(q)
except it returns None
if there are no more values instead
of raising the StopIteration
exception.with
statement), which closes the
query when the query goes out of scope or when an error happens.
Class janus.apply() is similar to janus.apply_once(), calling a Prolog predicate using functional notation style. It returns a Python iterator that enumerates all answers.
>>> list(janus.apply("user", "between", 1, 6)) [1, 2, 3, 4, 5, 6]
|
None janus.apply.next()apply
as an iterator is to be preferred. See discussion
above. Note that this calling convention cannot distinguish between the
Prolog predicate returning @none
and reaching the end of
the iteration.
Python provides access to dictionaries holding the local variables of
a function using locals() as well as the global variables stored
as attributes to the module to which the function belongs as
globals(). The Python C API provides
PyEval_GetLocals() and PyEval_GetGlobals(), but these
return the scope of the Janus API function rather than user code, i.e.,
the global variables of the janus
module and the local
variables of the running Janus interface function.
Python code that wishes Prolog to access its scope must pass the necessary scope elements (local and global variables) explicitly to the Prolog code. It is possible to pass the entire local and or global scope by the output of locals() and/or globals(). Note however that a dict passed to Prolog is translated to its Prolog representation. This representation may be prohibitively large and does not allow Prolog to modify variables in the scope. Note that Prolog can access the global scope of a module as attributes of this module, e.g.
increment :- py_call(demo:counter, V0), V is V0+1, py_setattr(demo, counter, V).
In traditional Prolog, queries succeed or fail. Systems that implement tabling with Well Founded Semantics such as XSB and SWI-Prolog define a third truth value typically called undefined. Undefined results may have two reasons; (1) the program is logically inconsistent or (2) restraints have been applied in the derivation.
Because classical Prolog truth is dominant, we represent the success
of a query using the Python booleans True
and False
.
For undefined answers we define a class janus.Undefined()
that may represent different levels of detail on why the result is
undefined. The notion of generic undefined is represented by a
unique instance of this class. The three truth values are accessible as
properties of the janus
module.
True
False
The class janus.Undefined() represents an undefined result under the Well Founded Semantics.
The class has a single property class term
that
represents either the delay list or the residual program.
See
janus.TruthVal() for
details.
True
. This is quite
pointless in the current design and this may go.janus.undefined
, a unique
instance of the class janus.Undefined().
The instances of this enumeration are available as attributed of the janus
module.
For example, given Russel's paradox defined in Prolog as below.
:- module(russel, [shaves/2]). :- table shaves/2. shaves(barber,P) :- person(P), tnot(shaves(P,P)). person(barber). person(mayor).
From Python, we may ask who shaves the barber in four ways as
illustrated below. Note that the Prolog representations for
janus.DELAY_LISTS
and janus.RESIDUAL_PROGRAM
use the write_canonical/1
notation. They may later be changed to use a more human friendly
notation.
# Using NO_TRUTHVALS >>> janus.query_once("russel:shaves(barber, X)", truth_vals=janus.NO_TRUTHVALS) {'truth': True, 'X': 'barber'} # Using default PLAIN_TRUTHVALS (default) >>> janus.query_once("russel:shaves(barber, X)") {'truth': Undefined, 'X': 'barber'} # Using default DELAY_LISTS >>> janus.query_once("russel:shaves(barber, X)", truth_vals=janus.DELAY_LISTS) {'truth': :(russel,shaves(barber,barber)), 'X': 'barber'} # Using default RESIDUAL_PROGRAM >>> janus.query_once("russel:shaves(barber, X)", truth_vals=janus.RESIDUAL_PROGRAM) {'truth': [:-(:(russel,shaves(barber,barber)),tnot(:(russel,shaves(barber,barber))))], 'X': 'barber'}
Class janus.Term() encapsulates a Prolog term. Similarly to the Python object reference (see py_is_object/1), the class allows Python to represent arbitrary Prolog data, typically with the intend to pass it back to Prolog.
prolog(Term)
to the data
conversion process. As a result, we can do
?- py_call(janus:echo(prolog(hello(world))), Obj, [py_object(true)]). Obj = <py_Term>(0x7f7a14512050). ?- py_call(print($Obj)). hello(world) Obj = <py_Term>(0x7f7a14512050).
Class janus.PrologError(), derived from the Python class Exception represents a Prolog exception that typically results from calling janus.query_once(), janus.apply_once(), janus.query() or janus.apply(). The class either encapsulates a string on a Prolog exception term using janus.Term. Prolog exceptions are used to represent errors raised by Prolog. Strings are used to represent errors from invalid use of the interface. The default behavior gives the expected message:
>>> x = janus.query_once("X is 3.14/0")['X'] Traceback (most recent call last): ... janus.PrologError: //2: Arithmetic: evaluation error: `zero_divisor'
At this moment we only define a single Python class for representing Prolog exceptions. This suffices for error reporting, but does not make it easy to distinguish different Prolog errors. Future versions may improve on that by either subclassing janus.PrologError or provide a method to classify the error more easily.