Problem with Calling Prolog from Python

I successfully did some Prolog from Python examples but failed on this one:

import janus_swi as janus

janus.consult("get", """
:- use_module(library(lists)).
frame(name(bird), isa(animal), [travel(flies), feathers], [ ]).
frame(name(penguin), isa(bird), [color(brown)], [travel(walks)]).
frame(name(canary), isa(bird), [color(yellow), call(sing)], [size(small)]).
frame(name(tweety), isa(canary), [ ], [color(white)]).
frame(name(opus), isa([penguin, cartoon_char]), [color(black)], [ ]).
get(Prop, Object) :- frame(name(Object), _, List_of_properties, _), member(Prop, List_of_properties).
get(Prop, Object) :- frame(name(Object), _, _, List_of_defaults), member(Prop, List_of_defaults).
get(Prop, Object) :- frame(name(Object), isa(Parent), _, _), get(Prop, Parent).
""")

q = janus.query("get(X, penguin).")
while ( s := q.next() ):
    print(s['X'])
q.close()         

I expected as output:
X = color(brown) ;
X = travel(walks) ;
X = travel(flies) ;
X = feathers ;
false.

But instead received following error:

Traceback (most recent call last):
  File "C:\Users\marti\OneDrive\Documents\Python\Swi-Prolog.py", line 19, in <module>
    while ( s := q.next() ):
  File "C:\Users\marti\AppData\Local\Programs\Python\Python310\lib\site-packages\janus_swi\janus.py", line 215, in next
    rc = _swipl.next_solution(self.state)
SystemError: <built-in function next_solution> returned NULL without setting an exception

Please help ! My Swi-Prolog is 64 bits, version 9.2.2
I am on Windows 11 64 bit with Python 3.10.6 64 bit
Regards Martin Klein

Your Prolog query returns a compound term and these have no representation in Python. The error could have been better :frowning: This works:

def run():
    q = janus.query("get(_X, penguin), term_string(_X,X)")
    while ( s := q.next() ):
        print(s['X'])
    q.close()         

Note that using term_string/2 we translate the result into a string and we use _X, starting with an underscore as intermediate variable that is not added to the result dict.

Be careful with this though. If anything goes wrong the query is not closed and Prolog gets upset. Better write this as below. Now the query is tightly scoped and Python will close it regardless of what happens. It is also shorter :slight_smile:

def run2():
    for s in janus.query("get(_X, penguin), term_string(_X,X)"):
        print(s['X'])
1 Like

Many thanks Jan,

This works perfectly ! Thanks for our quick help !
With kind regards. Martin

Pushed a fix to generate a proper exception. Still a bit cryptic though :frowning:

janus.PrologError: janus:py_call/2: Domain error: `py_term' expected, found `f(a)'

Python has something similar to Prolog’s setup_call_cleanup/3, namely “context managers”. One common usage is to open a file and have it automatically closed when not needed, e.g.:

with open("some file", "r") as infile, open("copy of some file", "w") as outfile:
   for line in file:
      outfile.write(line)

but something similar could be done for queries, so that everything gets cleaned up properly.

For more details, see the with statement, context managers, and contextlib.

1 Like

Good. How is this better/different from

for s in janus.query(...):
    ...

This construct seems to do the job nicely. Even when you “break” out of the loop or throw an exception it calls the __del__ method on the iterator, which closes the Prolog query. See packages-swipy/janus/janus.py at 9ca8c6a586d24fbe37c62dd4229c2efd69abfb62 · SWI-Prolog/packages-swipy · GitHub for the current implementation of query(). Forgive my Python and propose improvements if you see them :slight_smile:

Looking at context manager example, isn’t it the case that there are many things once can do with an open file and thus an iterator is not appropriate? While with a Prolog query there is little else one can do but iterate over the answers.

AFAIK, there’s no guarantee that __del__ is called at the end of the loop; that’s just an artefact of Cpython and its use of reference counting(*) – in fact, I’m not sure that __del__ is necessarily called at the end of the loop and not at the end of the enclosing function (Python has a rather weak notion of scoping other than function-level, although Python 3 does make the s in the for-loop invalid outside the loop).

In addition, if there’s an exception in the loop, __del__ doesn’t see it – __del__ will (eventually) be called, but all it can do is close the query.

You can see this by comparing

with open('...') as f:
  for line in f:
    ...

and

for line in open('...'):
   ...

The differences aren’t large (assuming that the file-like object that open() returns supports __del__() by doing a close()), even for situations where the open() throws an exception, but the with is more robust/flexible and generally preferred. There’s more rationale in the PEP343 document (and there’s even more discussion in the forums.

I think that all that’s required for janus.query() to support context managers (and be usable in a with construct) is that it supports __enter__() and __exit__() methods.

(*) CPython uses reference counting plus a mark&sweep GC for recursive data structures. However, there is nothing in the language specification that requires this and other implementations can be different (e.g., PyPy uses “an incremental, generational moving collector”); there is ongoing work on improving Python’s performance which is considering some changes to its memory management.

1 Like

Thanks. AFAIK, I did verify handling of exceptions to work properly. I indeed suspect this to depend on CPython’s scoping and reference counts. I don’t know how much of Janus will work on other Python implementations than CPython considering the extend of the C API used to realize it.

This looks like something we should support. Would it have to look like this?

with query("p(X)") as q:
    for a in q:
        print(a['X'])

I don’t really think this is a readability improvement :frowning: . Is there any way we can simplify this for the user?

I guess __enter__() will just return itself (as __iter__() does) and __exit__() calls close()?

But it would be good to have compatibility at the Python level.

It’s been a few years since I’ve written a context manager – IIRC, there are some subtleties, so I’ll read the PEP and other documentation (plus the janus code) before making a PR, which could take me a few days or more. I’d also want to think about what it means to pass q around as a generator object.

BTW, a context manager would allow, although I presume the current API also allows it:

with query('p(X)') as q:
  print(next(q)['X'])
1 Like

Please have a look. Input from a real Python programmer should help :slight_smile: The loop you show is IMO a little less clean than the for s in query():, but it is acceptable.

I’m preparing a PR (it seems to be straightforward after all), but first I want to improve a few of the tests, to make sure I’m not breaking anything. (Also, I want to look at query_once() to make sure it handles things properly.)

I agree that the with query('p(X)') as q is a bit cumbersome, but no worse than setup_call_cleanup/3 … both of these are the only way I know of to ensure that things get closed properly if there’s an error, or of ensuring that the query gets closed as soon as it’s not needed (there are no guarantees about when __del__ is called).

1 Like