12.
Adding Commands to Jess
The Java interface
jess.Userfunction represents a single
function in the Jess language. You can add new functions to the Jess
language simply by writing a class that implements the
jess.Userfunction interface (see below for details on how
this is done), creating a single instance of this class and installing
it into a
jess.Rete object using
Rete.addUserfunction(). The
Userfunction classes you
write can maintain state; therefore a
Userfunction can cache
results across invocations, maintain complex data structures, or keep
references to external Java objects for callbacks. A single
Userfunction can be a gateway into a complex Java subsystem.
12.1. Writing Extensions
I've made it as easy as possible to add user-defined functions to Jess.
There is no system type-checking on arguments, so you don't need to tell
the system about your arguments, and values are self-describing, so you
don't need to tell the system what type you return. You do, however, need
to understand several Jess classes, including
jess.Value,
jess.Context, and
jess.Funcall, as previously
discussed in the chapter
Introduction to Programming with Jess in Java.
12.1.1. Implementing your Userfunction
To implement the
jess.Userfunction interface, you need to implement
only two methods:
getName() and
call(). Here's an example
of a class called 'MyUpcase' that implements the Jess function
my-upcase,
which expects a String as an argument, and returns the string in uppercase.
import jess.*;
public class ExMyUpcase implements Userfunction {
// The name method returns the name by which
// the function will appear in Jess code.
public String getName() { return "my-upcase"; }
public Value call(ValueVector vv, Context context) throws JessException {
String result = vv.get(1).stringValue(context).toUpperCase();
return new Value(result, RU.STRING);
}
}
The
call() method does the business of your
Userfunction. When
call() is invoked, the first argument will
be a
ValueVector representation of the Jess code that evoked
your function. For example, if the following Jess function calls were
made,
Jess> (load-function ExMyUpcase)
Jess> (my-upcase foo)
"FOO"
the first argument to
call() would be a
ValueVector
of length two. The first element would be a
Value containing
the symbol (type
RU.SYMBOL)
my-upcase, and the second
argument would be a
Value containing the string
(
RU.STRING)
"foo".
Note that we use
vv.get(1).stringValue(context) to get
the first argument to
my-upcase as a Java String. If the
argument doesn't contain a string, or something convertible to a
string,
stringValue() will throw a
JessException
describing the problem; hence you don't need to worry about incorrect
argument types if you don't want to.
vv.get(0) will always
return the symbol
my-upcase, the name of the function being
called (the clever programmer will note that this would let you
construct multiple objects of the same class, implementing different
functions based on the name of the function passed in as a constructor
argument). If you want, you can check how many arguments your function
was called with and throw a JessException if it was the wrong number
by using the
vv.size() method. In any case, our simple
implementation extracts a single argument and uses the Java
toUpperCase() method to do its work.
call() must
wrap its return value in a
jess.Value object, specifying the
type (here it is
RU.STRING).
12.1.1.1. Legal return values
A
Userfunction must return a valid
jess.Value
object; it cannot return the Java
null value. To
return "no value" to Jess, use
nil. The value of
nil is available in the public static final
variable
jess.Funcall.NIL.
12.1.2. Loading your Userfunction
Having written this class, you can then, in your Java main
program, simply call
Rete.addUserfunction() with an instance
of your new class as an argument, and the function will be available
from Jess code. So, we could have
import jess.*;
public class ExAddUF {
public static void main(String[] argv) throws JessException {
// Add the 'my-upcase' command to Jess
Rete r = new Rete();
r.addUserfunction(new ExMyUpcase());
// This will print "FOO".
System.out.println(r.eval("(my-upcase foo)"));
}
}
C:\> java ExAddUF
"FOO"
Alternatively, the Jess language command
load-function could
be used to load
my-upcase from Jess:
Jess> (load-function ExMyUpcase)
Jess> (my-upcase foo)
"FOO"
12.1.3. Calling assert from a Userfunction
The
jess.Rete.assertFact() method has two overloads: one version
takes a
jess.Context argument, and the other does
not. When writing a Userfunction, you should always use
the first version, passing the
jess.Context
argument to the Userfunction. If you do not, your
Userfunction will not interact correctly with the
logical
conditional element.
12.2. Writing Extension Packages
The
jess.Userpackage interface is a handy way to group a collection
of
Userfunctions together, so that you don't need to install them
one by one (all of the extensions shipped with Jess are included in Userpackage
classes). A
Userpackage class should supply the one method
add(),
which should simply add a collection of
Userfunctions to a
Rete object using
addUserfunction(). Nothing mysterious going on, but it's very
convenient. As an example, suppose
MyUpcase was only one of a
number of similar functions you wrote. You could put them in a
Userpackage class like this:
import jess.*;
public class ExMyStringFunctions implements Userpackage {
public void add(Rete engine) {
engine.addUserfunction(new ExMyUpcase());
// Other similar statements
}
}
Now in your Java code, you can call
import jess.*;
public class ExAddUP {
public static void main(String[] argv) throws JessException {
// Add the 'my-upcase' command to Jess
Rete r = new Rete();
r.addUserpackage(new ExMyStringFunctions());
// This will still print FOO.
System.out.println(r.eval("(my-upcase foo)"));
}
}
C:\> java ExAddUP
"FOO"
or from your Jess code, you can call
Jess> (load-package ExMyStringFunctions)
Jess> (my-upcase foo)
"FOO"
to load these functions in. After either of these snippets, Jess
language code could call
my-upcase,
my-downcase, etc.
Userpackages are a great place to assemble a collection of
interrelated functions which potentially can share data or maintain references
to other function objects. You can also use
Userpackages to make
sure that your
Userfunctions are constructed with the correct
constructor arguments.
All of Jess's "built-in" functions are simply
Userfunctions,
albeit ones which have special access to Jess' innards. Most of them
are automatically loaded by code in the
jess.Funcall
class. You can use these as examples for writing your own Jess extensions.
12.3. Obtaining References to Userfunction Objects
Occasionally it is useful to be able to obtain a reference to an installed
Userfunction object. The method
Userfunction Rete.findUserfunction(String
name) lets you do this easily. It returns the
Userfunction
object registered under the given name, or null if there is none. This
is useful when you write Userfunctions which themselves maintain state
of some kind, and you need access to that state.