Environment Specific Functions

This tutorial is somewhat specific, however it shows how to exploit some features unique to PyCLIPS in order to accomplish a peculiar task. What we need is a simple way to define functions accessible to specific environments in CLIPS. Each function has to be visible in the environment in which it is defined, and not in the others.

We will wrap the clips.RegisterPythonFunction() utility in order to obtain this and develop a way to make the registration mechanism less prone to errors. Moreover, we will code this wrapper in a way that allows to "directly" call the defined function, bypassing the use of the "python-call" keyword when invoking CLIPS code. The most obvious way to achieve this goal, is to use a native CLIPS function, to be defined using the Environment.BuildFunction() utility.

First, let's analyze the proposed problem: we need functions bound to environments. Since functions in Python are mostly "global" and, in PyCLIPS, every registered Python function is visible to all environments, we will have to bind functions to environments within Python. A way to obtain this, is to store references to Python functions in a dictionary. Our dictionary will have environment identifiers as keys (we will return on this later).

# the following dictionary will contain the environment specific functions
ENV_SPECIFIC_FUNCTIONS = { }

Since environments do not have a built-in identifier (the Environment.Index property could lead to mistakes: the Environment object reuses CLIPS environments, and thus an index can correspond at different times to different environments) we will have to supply one of our own. Python provides an UUID library, so we will use it to ensure that each identifier is sufficiently unique. To make things more clear, we will encapsulate the generation of an identifier in a Python function:

# a Python function to create a simple Environment identifier: the initial
#  "eid-" prefix is just to ensure that the resulting string can be a SYMBOL
def env_id():
    return clips.Symbol("eid-" + str(uuid.uuid1()))

We will also have to ensure that the dictionary that we created above will take our environment into account. In order to be able to retrieve the environment identifier from the environment itself, we "create" a property for the Environment object from scratch. With excess of fantasy, we will call it Environment.Identifier:

# now we need some helpers to make it easier to set up the environment and
#  the map of environment specific functions
def PrepareEnvironment(e):
    eid = env_id()
    ENV_SPECIFIC_FUNCTIONS[eid] = { }   # a map of functions
    e.Identifier = eid                  # so that we can always get it back
    return eid

The ENV_SPECIFIC_FUNCTIONS entry is a dictionary too: it will bind function names to Python funcions. This way, we will be able to refer to a given Python function once we know which environment it belongs to and under which name it is (globally) known to CLIPS. In other words, we will have to register a single Python function in CLIPS that, given an environment identifier and a function identifier, retrieves the correct Python function, calls it and returns its result to CLIPS in turn.

# ...and this wrapper calls in turn the functions associated with certain
#  names for each environment
def envCallSpecificFunction(e_id, funcname, *args):
    f = ENV_SPECIFIC_FUNCTIONS[e_id][funcname]
    return f(*args)
clips.RegisterPythonFunction(
    envCallSpecificFunction, 'env-call-specific-func')

This way, each time we will have to call a function bound to an environment, if we know the environment identifier (and of course the name under which the function is stored in the dictionary), we will "just" have to issue a call like this:

(python-call env-call-specific-func
    eid-06bb07f0-220e-11dd-b310-005056c00008 registeredFunctionName
    arg1 ... argN)

This is obviously less than optimal: it adds complexity to the written code instead of making it more clear and specific. This is where the definition of a native CLIPS function would come to aid. Python is a quite introspective language, and this allows us to retrieve the number of arguments of the Python function to make visible to a certain environment in order to define a native CLIPS function with the same form:

def AddSpecificFunction(e, func, funcname=None):
    try:
        eid = e.Identifier
    except: raise ValueError("The Environment has not been prepared")
    if funcname is None:
        funcname = func.__name__    # if needed
    ENV_SPECIFIC_FUNCTIONS[eid][funcname] = func
    num_args = func.func_code.co_argcount
    seq_args = " ".join(['?a%s' % x for x in range(num_args)])
    e.BuildFunction(
        funcname,
        seq_args,
        "(return (python-call env-call-specific-func %s %s %s))" % (
            eid, funcname, seq_args))

Please note how we built seq_args: it's a space-delimited string containing elements of the form "?a0", ..., "?aN" (where N is one less than the number of arguments of the original Python function, since we start with 0). This can be done without loss of meaning because CLIPS only has positional arguments, and not named ones. Note that the function body actually hides the complexity that had been noticed above.

Also, notice that this definition allows to give the same name, in different environments, to different functions: for instance, if we want to call "sum" a certain function in an environment, we can have another function called "sum" in another one and referring to a different function, probably with a different name and possibly with a different number of arguments, since we can specify the name of the native CLIPS function, as native functions are naturally bound to environments.

The following code can be saved as a module. It's obviously quite rough, but it can be made more functional and accurate. Also, the module below contains some test code that can be used as an example.

# environment specific function wrapper (PoC)
 
import clips
import uuid
 
# a Python function to create a simple Environment identifier: the initial
#  "eid-" prefix is just to ensure that the resulting string can be a SYMBOL
def env_id():
    return clips.Symbol("eid-" + str(uuid.uuid1()))
 
# the following dictionary will contain the environment specific functions
ENV_SPECIFIC_FUNCTIONS = { }
 
 
# ...and this wrapper calls in turn the functions associated with certain
#  names for each environment
def envCallSpecificFunction(e_id, funcname, *args):
    f = ENV_SPECIFIC_FUNCTIONS[e_id][funcname]
    return f(*args)
clips.RegisterPythonFunction(envCallSpecificFunction, 'env-call-specific-func')
 
 
# now we need some helpers to make it easier to set up the environment and
#  the map of environment specific functions
def PrepareEnvironment(e):
    eid = env_id()
    ENV_SPECIFIC_FUNCTIONS[eid] = { }   # a map of functions
    e.Identifier = eid                  # so that we can always get it back
    return eid
 
def AddSpecificFunction(e, func, funcname=None):
    try:
        eid = e.Identifier
    except: raise ValueError("The Environment has not been prepared")
    if funcname is None:
        funcname = func.__name__    # if needed
    ENV_SPECIFIC_FUNCTIONS[eid][funcname] = func
    num_args = func.func_code.co_argcount
    seq_args = " ".join(['?a%s' % x for x in range(num_args)])
    e.BuildFunction(
        funcname,
        seq_args,
        "(return (python-call env-call-specific-func %s %s %s))" % (
            eid, funcname, seq_args))
 
 
# and now a test
 
if __name__ == "__main__":
    # create a couple of environments and prepare them to use specific
    #  functions
    e1 = clips.Environment()
    e2 = clips.Environment()
    PrepareEnvironment(e1)
    PrepareEnvironment(e2)
    # note that we discarded the return values: it's not a problem for us
    #  since the environments will keep their ID in the Identifier member
    #  variable
 
    # let's create two different functions to be used in the two environments
    #  (here they have different names, but when defined in different Python
    #  modules they could even share the same name, and this is mostly the
    #  point I think); in the following lines "f2a" stands for "function with
    #  two arguments" and eX stands obviously for "Environment X"
    def f2a_e1(a, b):
        return a + b
    def f2a_e2(a, b):
        return a - b
 
    # so, let's "register" the given functions with the two environments...
    AddSpecificFunction(e1, f2a_e1, "f2a")  # we want the same name, it will
    AddSpecificFunction(e2, f2a_e2, "f2a")  #  be 'f2a' in both environments
 
 
    # ...and this is the test
    print e1.Eval("(f2a 42 13)")    # should be 55
    print e2.Eval("(f2a 42 13)")    # should be 29
    # and it works exactly as expected! ;-)
 
 
# end.