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 startIt works like this:
| (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)))))
CLIPS> (load "sum.clp")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.
!!!$*
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
Here are the CLIPS deffunctions:
|(deffunction printout1 (?logical-name $?args)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:
| (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)))
|import clipsOnce 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.
|
|def pyprintout(*args):
| for arg in args[1]:
| if arg.cltypename().upper() == "SYMBOL":
| if arg.upper() == "CRLF":
| 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)
C:\...>python sum.pyThe 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.
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:\...>
[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.