Subsections


A. Usage Notes


A.1 Environments

As seen in the detailed documentation, PyCLIPS also provides access to the environment features of CLIPS, through the Environment class. Environment objects provide almost all functions normally available at the top level of PyCLIPS, that is importing clips into a Python script. Environment object methods having the same name of functions found at the top level of PyCLIPS, have the same effect of the corresponding function - but restricted to the logical environment represented by the object itselfA.1. Normally in a true CLIPS session users would not use environments, so the concept of environment may, in many cases, not be useful.

There are also functions (namely: CurrentEnvironment() and Environment.SetCurrent()) that allow the user to switch environment and to use top level functions and classes in any of the created environments. This is useful in cases where environments are used, because due to the double nature of CLIPS API (see Clips Reference Guide Vol. II: Advanced Programming Guide about companion functions for standard API), objects defined in environments have slightly different types than corresponding top level objects - since these types are environment-aware. However environment aware classes define exactly the same methods as the top level counterparts, so their logical use is the same.

Note: Please note that the CurrentEnvironment() function returns a fully functional Environment object. However the system preventsA.2 invasive access via environment-aware functions to current environment: user code should always use functions defined at module level to access current environment. The Environment methods become safe as soon as another Environment has become current - and in this case its methods and properties will raise an error.

Environments are a limited resource: this is because it is impossible in PyCLIPS to destroy a created Environment. In order to reuse Environments it may be useful to keep a reference to each of them for the whole PyCLIPS session. If there is an attempt to create more Environments than allowed, an exception is raised.


A.2 Multiple Ways

There is more than one way to use the PyCLIPS module, since it exposes almost all the API functions of CLIPS, seen in fact as a library, to the Python environment.

The module PyCLIPS provides some functions, that is Build() and Eval(), that let the user directly issue commands in the CLIPS subsystem from a Python program. In fact, the use of Build() allows definition of constructs in CLIPSA.3, and Eval() lets the user evaluate values or call code directly in the subsystem. So for instance rules can be built in PyCLIPS using

>>> import clips
>>> clips.Build("""
(defrule duck-rule "the Duck Rule"
   (duck)
   =>
   (assert (quack)))
""")
>>> clips.PrintRules()
MAIN:
duck-rule

and evaluate a sum using

>>> n = clips.Eval("(+ 5 2)")
>>> print n
7

Also, the user is allowed to call functions that do not return a value using Eval()A.4, as in the following example:

>>> clips.Eval('(printout t "Hello, World!" crlf)')
>>> print clips.StdoutStream.Read()
Hello, World!

There is another function, namely SendCommand(), that sends an entire CLIPS command (it has to be a full, correct command, otherwise PyCLIPS will issue an exception): as Build() it does not return any value or objectA.5. However this can be particularly useful when the user needs to implement an interactive CLIPS shell within an application built on PyCLIPS. Unless the application is mostly CLIPS oriented (if for instance Python is used just as a ``glue'' script language) probably the use of this function has to be discouraged, in favour of the code readability that - at least for Python programmers - is provided by the Python oriented interface.

Using SendCommand() it becomes possible to write:

>>> import clips
>>> clips.SendCommand("""
(defrule duck-rule "the Duck Rule"
   (duck)
   =>
   (assert (quack)))
""")
>>> clips.SendCommand("(assert (duck))")
>>> clips.Run()
>>> clips.PrintFacts()
f-0     (duck)
f-1     (quack)
For a total of 2 facts.
>>> clips.PrintRules()
MAIN:
duck-rule

The most important caveat about SendCommand() is that CLIPS accepts some kinds of input which normally have to be considered incorrect, and PyCLIPS does neither return an error value, nor raise an exception: for instance, it is possible to pass a symbol to CLIPS to the command line as in

CLIPS> thing
thing

and in this case CLIPS ``evaluates'' the symbol, printing it to the console as a result of the evaluation. PyCLIPS does not automatically capture evaluation output, and just accepts a symbol (or other commands that can be evaluated) as input without any production:

>>> import clips
>>> clips.SendCommand("thing")

but, turning on the verbosity flag:

>>> clips.SendCommand("thing", True)
>>> print clips.StdoutStream.Read()
thing

Of course, PyCLIPS complains if something incorrect is passed to the SendCommand() function and raises an exception as previously stated. However the exception is accompanied by a rather non-explanatory text. The ErrorStream object should be queried in this case in order to obtain some more information about the error:

>>> clips.SendCommand("(assert (duck)")	# no right bracket

Traceback (most recent call last):
  File "<pyshell#5>", line 1, in -toplevel-
    clips.SendCommand("(assert (duck)")	# no right bracket
  File ".../_clips_wrap.py", line 2575, in SendCommand
    _c.sendCommand(s)
ClipsError: C09: unable to understand argument
>>> print clips.ErrorStream.Read()

[PRNTUTIL2] Syntax Error:  Check appropriate syntax for RHS patterns.

Obviously SendCommand() can lead to serious errors if not used with some kind of interaction.

The point of this paragraph is that, for entity definition (evaluation can only be performed using the Eval() or Call() functions), the PyCLIPS module provides a full set of specific BuildEntity() functions which also return appropriate objects corresponding to specific entities. So, the task of building a rule in CLIPS (in fact, a Rule object in Python) could preferably be performed directly using the BuldRule() function, that is:

>>> clips.Clear()
>>> clips.Reset()
>>> r0 = clips.BuildRule("duck-rule", "(duck)", "(assert (quack))",
                         "the Duck Rule")
>>> print r0.PPForm()
(defrule MAIN::duck-rule "the Duck Rule"
   (duck)
   =>
   (assert (quack)))

>>> clips.PrintRules()
MAIN:
duck-rule

thus with the same effect as with the Build() function, but obtaining immediately a reference to the rule entity in CLIPS as a Python object. Similar examples could be provided for the SendCommand() function, using the appropriate constructs or commands that can be used to achieve the same goals.

This allows the user to choose between at least two programming styles in PyCLIPS: the former, more CLIPS oriented, relies heavily on the use of the Build(), Eval() and SendCommand() functions, and is probably more readable to CLIPS developers. The latter is somewhat closer to Python programming style, based on the creation of objects of a certain nature by calling specific Python functions. The advice is to avoid mixing the two styles unless necessary, since it can make the code quite difficult to understand.


A.3 Python Functions in CLIPS

In PyCLIPS it is possible to execute Python functions from within CLIPS embedded constructs and statements. This allows the extension of the underlying inference engine with imperative functionalities, as well as the possibility to retrieve information from the Python layer asynchronously with respect to Python execution. Of course this possibility enables some enhancements of the CLIPS environment, but - as a drawback - it also opens the way to errors and misunderstandings.

Usage of Python external functions is fairly simple: the user should register functions that will be called from within the CLIPS subsystem in PyCLIPS using the RegisterPythonFunction() toplevel function. If no alternate name for the function is specified, then the Python name will be usedA.6. If necessary, Python function names can be deregistered using UnregisterPythonFunction() and ClearPythonFunctions() utilities. Once a function is registered it can be called from within the CLIPS engine using the following syntax:

    (python-call <funcname> [arg1 [arg2 [ ... [argN]]]])

and will return a value (this allows its use in assignments) to the CLIPS calling statement. In the call, <funcname> is a symbol (using a string will result in an error) and the number and type of arguments depends on the actual Python function. When arguments are of wrong type or number, the called function fails. Using the previously illustrated py_square example, we have:

>>> clips.RegisterPythonFunction(py_square)
>>> clips.SetExternalTraceback(True)	# print traceback on error
>>> print clips.Eval("(python-call py_square 7)")
49
>>> print clips.Eval('(python-call py_square "a")')
Traceback (most recent call last):
  File ".../_clips_wrap.py", line 2702, in <lambda>
    f = lambda *args: _extcall_retval(func(*tuple(map(_cl2py, list(args)))))
  File "<pyshell#85>", line 2, in py_square
TypeError: can't multiply sequence to non-int
FALSE
>>> print clips.Eval("(python-call py_square 7 7)")
Traceback (most recent call last):
  File ".../_clips_wrap.py", line 2702, in <lambda>
    f = lambda *args: _extcall_retval(func(*tuple(map(_cl2py, list(args)))))
TypeError: py_square() takes exactly 1 argument (2 given)
FALSE

It is important to know, in order to avoid errors, that the Python interpreter that executes functions from within CLIPS is exactly the same that calls the PyCLIPS function used to invoke the engine: this means, for example, that a Python function called in CLIPS is subject to change the state of the Python interpreter itself. Moreover, due to the nature of CLIPS external function call interface, Python functions called in CLIPS will never raise exceptionsA.7 in the Python calling layer.

Here are some other issues and features about the nature of the external function call interface provided by PyCLIPS:

Functions should be CLIPS-aware: when CLIPS calls a Python external function with arguments, these are converted to values that Python can understand using the previously described wrapper classes. Thus, for instance, if the Python function is given an integer argument, then an argument of type Integer (not int) will be passed as actual parameter. This means that in most cases PyCLIPS has to be imported by modules that define external Python functions.

Actual parameters cannot be modified: there is no way to pass values back to CLIPS by modifying actual parameters. The possibility to use Multifield parameters as lists should not deceive the user, as every modification performed on Multifields that Python receives as parameters will be lost after function completion. A way to handle this is to treat parameters as immutable values.

External functions should always return a value: functions always return a value in CLIPS, even in case of an error. This can be clearly seen in the following chunk of CLIPS code:

CLIPS> (div 1 0)
[PRNTUTIL7] Attempt to divide by zero in div function.
1

where, although an error message is printed to the console, the value 1 is returned by the system. In the same way, CLIPS expects Python external functions to return a value. PyCLIPS solves this issue by converting a return value of None (which is the real return value for Python functions that simply return) into the symbol nil, that has a meaning similar to the one of None for Python. Also, functions that raise uncaught exceptions will in fact return a value to the underlying CLIPS engine: in this case the returned value is the symbol FALSE, and an error message is routed to the error stream - thus, it can be retrieved using ErrorStream.Read(). The following example imitates the div CLIPS example above:

>>> import clips
>>> exceptor = lambda : 1 / 0
>>> clips.RegisterPythonFunction(exceptor, 'exceptor')
>>> clips.SetExternalTraceback(True)	# print traceback on error
>>> clips.Eval('(python-call exceptor)')
Traceback (most recent call last):
  File ".../_clips_wrap.py", line 2702, in <lambda>
    f = lambda *args: _extcall_retval(func(*tuple(map(_cl2py, list(args)))))
  File "<pyshell#79>", line 1, in <lambda>
ZeroDivisionError: integer division or modulo by zero
<Symbol 'FALSE'>

Return values must be understood by CLIPS: only values that can be converted to CLIPS base types can be returned to the inference engine. This includes all values that can be converted to PyCLIPS wrapper classes. In fact it can be considered a good practice to cast return values to PyCLIPS wrapper classes when the main purpose of a function is to be called from within CLIPS.

Python functions act as generic functions: due to the nature of Python, functions are generally polymorphic:

>>> def addf(a, b):
        return a + b
>>> print addf("egg", "spam")
eggspam
>>> print addf(2, 4)
6

The intrinsic polymorphism of Python functions is kept within the CLIPS subsystem:

>>> import clips
>>> clips.RegisterPythonFunction(addf)
>>> print clips.Eval('(python-call addf "egg" "spam")')
eggspam
>>> print clips.Eval('(python-call addf 2 4)')
6

Thus Python functions act in a way that is similar to generics.



Footnotes

... itselfA.1
In fact, the Python submodule that implements the Environment class is generated automatically: the process can be examined by looking at the code in setup.py and clips/_clips_wrap.py.
... preventsA.2
Raising an appropriate exception.
... CLIPSA.3
Note that the Build() function does not return any value or object, so you will have to call FindConstruct() to find entities created using the Build() function.
...Eval()A.4
There is a discussion about functions that only have side effects in CLIPS, such as printout, in Clips User's Guide, that is, the CLIPS tutorial.
... objectA.5
Some information about the command result can be retrieved reading the appropriate output streams.
... usedA.6
An example of function registration has been provided in the introduction.
... exceptionsA.7
Exceptions can arise during the Python function execution, and can be caught inside the function code. However, for debugging purposes, there is the possibility to force PyCLIPS print a standard traceback whenever an error occurs in a Python function called by CLIPS.