2008-07-14

Using printout and readline in PyCLIPS applications

I am trying to help a CLIPS user compile his application into a Windows executable (.exe) by following the description in one of my previous blog posts. But his application relies on the built in functions read and printout. And my blog post says nothing about that type of situation since all of the times I've done this before have been with applications that have GUIs and I've never had to bother with command line interaction.

The problem is, more specifically, that PyCLIPS consumes all calls to printout and read without printing to screen or requiring any user input. This may seem like a stupid design decision at first, but if you consider the many situations and environments that PyCLIPS can run in. It starts to feel like a rather sensible approach since it's obviously impossible to know what the right thing to do is. If you need user interaction via command line, you can provide Python functions that perform I/O for you.

So. Just to be clear. PyCLIPS does not constrain the possibilities for interaction with CLIPS in any way. It just doesn't make any assumptions about how it should best be done.

Ok. Fair enough. But, what to do?

First off. I don't want to have to maintain different versions of the application just to perform I/O. I need a way to dynamically dispatch I/O to either the built in functions (if it's run in CLIPS Dialog) or to my Python functions (if it's run in PyCLIPS).

Here is a simple application that uses printout and read:

|(deffacts start
| (sum 0))
|
|(defrule calc-sum
| ?sum <- (sum ?s)
| =>
| (retract ?sum)
| (printout t "Enter a number or Q to quit: ")
| (bind ?input (read))
| (if (numberp ?input)
| then (bind ?s (+ ?s ?input))
| (assert (sum ?s))
| else (if (or (eq ?input q)
| (eq ?input Q))
| then (printout t "Sum = " ?s crlf)
| else (printout t "Invalid input: " ?input crlf)
| (assert (sum ?s)))))
It works like this:
CLIPS> (load "sum.clp")
!!!$*
TRUE
CLIPS> (reset)
CLIPS> (run)
Enter a number or Q to quit: one
Invalid input: one
Enter a number or Q to quit: 9
Enter a number or Q to quit: 8
Enter a number or Q to quit: 5
Enter a number or Q to quit: Q
Sum = 22
When I first thought about how to make this program run (with as little effort as possible) in PyCLIPS, I didn't think that it would be very difficult. I was actually quite certain I'd be able to monkey-patch PyCLIPS with my I/O functions. CLIPS, however, was far from impressed with my attempt. So, a slap on the wrist later, I settled on (manually) replacing all function calls to printout, readline and read to deffunction wrappers (printout1, readline1 and read1) instead.

Here are the CLIPS deffunctions:
|(deffunction printout1 (?logical-name $?args)
| (if (member$ python-call (get-function-list))
| then (funcall python-call pyprintout ?logical-name $?args)
| else (progn$ (?arg $?args)
| (printout ?logical-name ?arg))))
|
|(deffunction readline1 ($?logical-name)
| (if (> (length$ $?logical-name) 0)
| then (bind ?logical-name (first$ $?logical-name))
| else (bind ?logical-name t))
|
| (if (member$ python-call (get-function-list))
| then (funcall python-call pyreadline ?logical-name)
| else (readline ?logical-name)))
|
|(deffunction read1 ($?logical-name)
| (if (> (length$ $?logical-name) 0)
| then (bind ?logical-name (first$ $?logical-name))
| else (bind ?logical-name t))
|
| (if (member$ python-call (get-function-list))
| then (eval (funcall python-call pyread ?logical-name))
| else (read ?logical-name)))
They look quite hairy. I know. But they're generic so it's not something you'd have to write and modify for each application that you want to be able to run in both CLIPS and PyCLIPS. There's also some Python boiler plate. It looks like this:
|import clips
|
|def pyprintout(*args):
| for arg in args[1]:
| if arg.cltypename().upper() == "SYMBOL":
| if arg.upper() == "CRLF":
| print
| elif arg.upper() == "TAB":
| print "\t",
| else:
| print arg,
|
| else:
| print arg,
|
|def pyreadline(*args):
| return raw_input()
|
|def pyread(*args):
| return raw_input()
|
|clips.RegisterPythonFunction(pyprintout)
|clips.RegisterPythonFunction(pyreadline)
|clips.RegisterPythonFunction(pyread)
Once we've got all of the above in place. We can load, reset and run from within a PyCLIPS script and have the application work directly in the Windows command line.
C:\...>python sum.py
Enter a number or Q to quit: one
Invalid input: one
Enter a number or Q to quit: 9
Enter a number or Q to quit: 8
Enter a number or Q to quit: 5
Enter a number or Q to quit: Q
Sum = 22

C:\...>
The files sum.py and sum.clp contain the whole application if you're interested. The point of all this is of course to be able to compile the PyCLIPS application into an executable. Step for step instructions on how to do that can be found here.

[Update 2008-07-14] I just fixed the pyprintout function (in sum.py), so that it also prints symbols. The code I added is marked with red.

1 kommentar:

Almost Earthling sa...

Hi Johan,

I will not pretend that we didn't discuss this privately before, as we both agreed that an explanation of what lead me to certain implementation choices is needed. I think, however, that a better approach is to propose a simple solution to the problem as an introduction to my comment. I have to say, that this is neither a complete nor an elegant solution, however it should be sufficient to handle a reasonable number of cases. It's based on the possibility that CLIPS gives, to run a program stepwise. In fact, you can call clips.Run(1) in order to only execute one step of inference in the CLIPS engine. clips.Run(limit) has a return value, that reports the number of actually performed inference steps: at the end it returns less than limit, zero in our case. When clips.Run() returns, all CLIPS "streams" can be read and examined, and you can, for example, print the results to the console. So, with a code snippet similar to the following:

while(clips.Run(1) != 0):
    out = clips.StdoutStream.Read()
    err = clips.ErrorStream.Read()
    do_something_with(out, err)


you can partially achieve the goal of loading a CLIPS program, intercepting its output and redirecting it to whatever you like. The only requirement is that the CLIPS program should not contain a call to the (run) function. Obviously this only solves the problem of capturing CLIPS output: interactive user input is not handled at all, which still poses a problem if you need to bundle an application into an executable. In this case your solution is still the most viable one.

Now I'll try to explain why PyCLIPS does not provide a direct way to interact with the underlying CLIPS engine I/O. In fact, PyCLIPS wasn't intended as a replacement to the CLIPS shell in the first place, it was conceived as an interface to the CLIPS API: that is, CLIPS is considered in PyCLIPS more as a library. Apart from some little additions, the "low-level part" of PyCLIPS just maps the API functions described in the Advanced Programming Guide one-by-one to Python. With the "high-level" module I just tried to provide the Python user with a more "pythonic" interface, made of classes and general use functions. Since CLIPS gave the possibility, I also tried to implement some utilities to assist a Python programmer during PyCLIPS interactive use, such as the PrintXxxxx functions. I also implemented a method (namely SendCommand) to issue commands directly to the CLIPS engine, as if it were an interactive session, but this is not an API function and you will not find it in the APG.

Initially, since the CLIPS engine provides a lot of useful output, I thought that it would be nice to give the possibility to the Python user to intercept the calls to I/O routines, the ones for reading and printing respectively from and to the console. However, CLIPS has a rather complex way of handling basic (and file) I/O. It uses "logical names" for the files and the basic I/O streams, and the functions (printout ...) and (read) are used for writing and reading. Apart from files, whose namespace exists in PyCLIPS separated from the one of Python, there are other "I/O routers" that handle console I/O. Opposedly to usual systems, where there are three I/O streams (stdin, stdout and stderr), here we have streams also for the prompt, for the trace messages and more in addition to the usual three ones. This initially made me think that it would have been quite difficult to allow a Python user to optionally redirect basic I/O to the console. So I ended up thinking that the possibility to specify a function to call whenever a character was written to some stream (or expected from some other) could have been a solution: in other words, a callback or a hook. However this also poses some problems. Mostly these two:

1) slowness: when such a function is provided, each character has to be examined by it. There are already some performance issues that I'm trying to solve, and this implementation could have a heavy impact on an already slowed down system;

2) the fact that the Python and CLIPS subsystems are separated and do their reasoning in different ways: a provided callback should have a well defined signature (for those unfamiliar with C/C++ terminology, signature is related to the parameter types combined with the type of the return value). But Python is dynamically typed, so I cannot check the parameter types in a function, and I surely am not able to determine its return value without executing it. Mandatory type checks are left to the function implementor, that is, the module user... a Python user cannot be expected to take the same precautions that a C/C++ programmer would though. A Python user expects the exception system to be a valuable aid in finding bugs and handling exceptional situations. Well, in the case of callbacks exceptions are also more of a problem than an aid: when Python bails out with an exception, if the interpreter is in the middle of a callback, it could leave either CLIPS or Python (or both) in an inconsistent state, while the user would generally not know for sure whether the program is executing inside or outside the CLIPS engine.

These seemed to me quite difficult issues to cope with, and at the time of implementation I decided to mantain the router system intact, as it is documented in the Advanced Programming Guide. So, to monitor CLIPS output, the PyCLIPS user has still to periodically check the streams, and for direct input... there are other PyCLIPS tricks.

One could argue, that PyCLIPS implements callbacks in a feature that seems to be one of the most appreciated ones: the possibility to call Python code from CLIPS (via the (python-call ...) construct). Well: strangely, this shows actually less problems than a specific callback! In fact, when you call a Python function from CLIPS, from a PyCLIPS user point of view no type checking is performed on function parameters and return values: everything is left to the CLIPS code for return values and to the Python function for arguments. Exceptions aren't handled at their best though, they are ignored when uncaught - with the possibility to force the output of a traceback (which doesn't stop the CLIPS program flow). Coherently with usual CLIPS behaviour, functions that raise uncaught exceptions just return the SYMBOL FALSE to the CLIPS engine.

This for the explanations. However, you suggested that there are many users that want to use PyCLIPS as a framework, as well as a way to directly execute CLIPS programs (for instance, to embed the programs in an executable package or a web application), and (I would add) as an interactive shell too. I'm flattered: this means that PyCLIPS is really flexible enough to allow its use in ways that I wouldn't initially imagine. This also means that I'll have to put some effort in solving this problem: I don't know right now what I can do, but I'll try to find out something. If there is a possibility to act at the Python level (as you propose) it's much better, because I wouldn't like to populate the low-level module with too many non-API functions.

I hope that this comment could be still useful, despite its excessive length!

I hope that we'll find a way to improve PyCLIPS usability.

Cheers,

F.