- Documentation
- Reference manual
- Packages
- SWI-Prolog Python interface
- Introduction
- Data conversion
- Janus by example
- library(janus): Call Python from Prolog
- Calling Prolog from Python
- Janus and threads
- Janus as a Python package
- Prolog and Python
- Janus performance evaluation
- Python or C/C++ for accessing resources?
- Janus platforms notes
- Compatibility to the XSB Janus implementation
- Status of Janus
- SWI-Prolog Python interface
5 Calling Prolog from Python
The binding can also call Prolog from Python. This can both be used to realize call backs, i.e., the Python system embedded into Prolog calls Prolog, or after embedding SWI-Prolog into Python.
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 advice to import
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 consider the module to be named
janus.
The Python module janus provides utility functions and
defines the classes janus.Query(), janus.Term()
and
janus.PrologError(). We
start our discussion by introducing the janus.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.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.once("Y is 1+1", {}) # Ok for "static" queries
{'Y': 2, 'truth': True}
>>> x = 1
>>> janus.once(f"Y is {x}+1", {}) # Do not use this
{'Y': 2, 'truth': True} # See above
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.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
truth3Note 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 the string ’Undefined'.
If evaluation of the query failed, all variable bindings are bound to
the Python constant None and the truth key is False.
The following Python function returns True if the Prolog
system supports unbounded integers and False otherwise.
def hasBigIntegers():
janus.once("current_prolog_flag(bounded,false)")['truth']
While janus.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.once(), the returned dict
contains a truth field. This field cannot be False
though and thus is either
True or the string 'Undefined'.4The
representation of Undefined is still under discussion.
import janus_swi as janus
def printRange(from, to):
for d in janus.Query("between(F,T,X)", {"F":from, "T":to})
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
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()
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.
- dict janus.once(query, bindings={}, keep=False, truth=TruthVals.PLAIN_TRUTHVALS)
- Call query using bindings as once/1,
returning a dict with the resulting bindings. If bindings is
omitted, no variables are bound. The keep parameter
determines whether or not Prolog discards all backtrackable changes. By
default, such changes are discarded and as a result, changes to
backtrackable global variables are lost. Using
True, such changes are preserved.>>> once("b_setval(a, 1)", keep=True) {'truth': 'True'} >>> 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 keytruth5As this name is not a valid Prolog variable name, this cannot be ambiguous. that has the valueFalse(query failed, all bindings areNone),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 thetruthparameter. Below is an example. See section 5.2 for details.>>> import janus_swi as janus >>> janus.once("undefined") {'truth': Undefined} - None janus.consult(file, data=None, module='user')
- Load Prolog text into the Prolog database. By default, data
is
Noneand 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")]) - None janus.prolog()
- Start the interactive Prolog toplevel. This is the Python equivalent of py_shell/0.
5.1 Janus class Query
Class janus.Query() is similar to the janus.once() function, but it returns a Python iterator that allows for iterating over the answers to a non-deterministic Prolog predicate.
- Query janus.Query(query, bindings={}, keep=False)
- As janus.once(), returning an iterator
that provides an answer dict as janus.once()
for each answer to query. Answers never have
truthFalse. See discussion above. - dict
|None janus.Query.next() - Explicitly ask for the next solution of the iterator. Normally, using
the
Queryas an iterator is to be preferred. See discussion above. - None janus.Query.close()
- Close the query. Closing a query is obligatory. When used as an iterator, the Python destructor (__del__()) takes care of closing the query.
5.2 Janus and Prolog truth
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.
- janus.true
- This property has the Python boolean
True - janus.false
- This property has the Python boolean
False - janus.undefined
- This property holds a unique instance of class janus.Undefined()
5.2.1 Janus classed Undefined and TruthVal
The class janus.Undefined() represents an undefined result under the Well Founded Semantics.
- Undefined janus.Undefined(term=None)
- Instances are never created explicitly by the user. They are created by
the calls to Prolog initiated from janus.once()
and janus.Query().
The class has a single property class
termthat represents either the delay list or the residual program. See janus.TruthVal() for details. - Enum janus.TruthVal()
- This class is a Python enumeration. Its values are passed as
the optional truth parameter to janus.once()
and
janus.Query(). The defined
instances are
- NO_TRUTHVALS
- Undefined results are reported as
True. This is quite pointless in the current design and this may go. - PLAIN_TRUTHVALS
- Return undefined results as
janus.undefined, a unique instance of the class janus.Undefined(). - DELAY_LISTS
- Return undefined results as an instance of class janus.Undefined(). thats holds the delay list in Prolog native representation. See call_delays/2.
- RESIDUAL_PROGRAM
- Return undefined results as an instance of class janus.Undefined(). thats holds the residual program in Prolog native representation. See call_residual_program/2.
The instances of this enumeration are available as attributed of the
janusmodule.
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.once("russel:shaves(barber, X)", truth=janus.NO_TRUTHVALS)
{'truth': True, 'X': 'barber'}
# Using default PLAIN_TRUTHVALS (default)
>>> janus.once("russel:shaves(barber, X)")
{'truth': Undefined, 'X': 'barber'}
# Using default DELAY_LISTS
>>> janus.once("russel:shaves(barber, X)", truth=janus.DELAY_LISTS)
{'truth': :(russel,shaves(barber,barber)), 'X': 'barber'}
# Using default RESIDUAL_PROGRAM
>>> janus.once("russel:shaves(barber, X)", truth=janus.RESIDUAL_PROGRAM)
{'truth': [:-(:(russel,shaves(barber,barber)),tnot(:(russel,shaves(barber,barber))))], 'X': 'barber'}
5.3 Janus class Term
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.
- Term janus.Term(...)
- Instances are never created explicitly by the user. An instance is
created by handling a term
prolog(Term)tho the data conversion processes. 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). - Term janus.Term.__str__()
- Return the output of print/1 on the term. This is what is used by the Python function print().
- Term janus.Term.__repr__()
- Return the output of write_canonical/1 on the term.
5.4 Janus class PrologError
Class janus.PrologError(), derived from the Python class Exception represents a Prolog exception that typically results from calling janus.once() or using janus.Query(). 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.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.
- PrologError janus.PrologError(TermOrString)
- The constructor may be used explicitly, but this should be very uncommon.
- String janus.PrologError.__str__()
- Return a human readable message for the error using message_to_string/2
- String janus.PrologError.__repr__()
- Return a formal representation of the error by means of write_canonical/1.