This report describes Jess, a clone of the popular CLIPS expert system shell written entirely in Java. Jess supports the development of rule-based expert systems which can be tightly coupled to code written in the powerful, portable Java language. The syntax of the Jess language is discussed, and a comprehensive list of supported functions is presented. A guide to calling Java functions from Jess, and to extending Jess by writing Java code, is also included.
Jess is compatible with all versions of Java starting with version 1.0.2. It is (in particular) Java 1.1 compatible, although while compiling you will see warnings about deprecated methods. Such is the price of compatibility!
Jess is a work in progress - more features are always being added. The order will be determined in part by what folks seem to want most, what I need Jess to do, and how much time I have to spend on it. See the Section Version History, for a list of what's new in this version of Jess, and see section 1.2 below for a quick overview.
There is a Jess email discussion list you can join. To get information about the jess-users list, send a message to majordomo@sandia.gov containing the text
help
info jess-users
end
as the BODY of the message.
This is the 4.0 official release. It is enormously faster than Jess 3.2. There may still be bugs. Please report any that you find to me at ejfried@ca.sandia.gov so I can fix them for a later release.
Jess is copyrighted software - see the file LICENSE for details.
uncompress Jess-4.0.tar.Z
tar xf Jess-4.0.tar
If you downloaded Jess for Windows, you get a .zip file which should be
unzipped using a Win32-aware unzip program like WinZip. Don't use PKUNZIP
- it cannot handle long file names. WinZip is nice.
When Jess is unpacked, you should have a directory named 'Jess40b4'. Inside this directory should be the following files:
| README.html | This file |
| jess/ | A directory containing the 'jess' package. There are many source files in here that implement Jess's inference engine; others implement a number of Jess GUIs and command-line interfaces. Main.java implements both the applet interface and the command-line interface. NullDisplay.java is used by the command-line version; QuizDisplay is a very simple GUI console for Jess with both an application and an applet personality. |
| jess/view | Java source implementing the optional Jess 'view' command. |
| jess/reflect | Java source implementing the optional Jess commands that let you create and manipulate Java objects from Jess. |
| examples/ | A directory of tiny example CLIPS files. |
| jess/examples | A directory of example Java files. |
| index.html | A web page containing the Jess example applet; it may need to be edited! |
| Makefile | A simple makefile for Jess. |
Jess comes as a set of Java source files. You'll need to compile them first. If you have a 'make' utility (any UNIX make, or nmake or GNU make on Win32), you can just run make and the enclosed makefile will build everything. Otherwise the commands
javac jess/*.java (UNIX)or
javac jess\*.java (Win32)would work just fine, given that you have a Java compiler like Sun's JDK. If you have problems, be sure that the directory in which the jess subdirectory appears is on your CLASSPATH; this may mean including '.' (dot). Don't try to compile from inside the Jess40b4/jess/ directory; it won't work. You can use either a Java 1.0.2 or a Java 1.1 compiler to compile Jess; the resulting code runs on either 1.0 or 1.1 VMs. Note that if you use a 1.1 compiler, you will see some warning about 'deprecated methods' - it is safe to ignore these warnings. I could make them go away, but then Jess would not be 1.0.2 compatible!
There are a number of optional source files in the subdirectories Jess40b4/jess/view/, Jess40b4/jess/reflect/ and Jess40b4/jess/examples/* that aren't compiled if you follow the instructions above. These files define the optional debugging command view, the 'reflection' commands new, call, set, get, set-member, and get-member, and the Java object matching commands defclass and definstance. They can be compiled only with Java 1.1 or later. If you have such a compiler, then you can issue a command like
javac jess/*.java jess/view/*.java jess/reflect/*.java jess/examples/*/*.javato compile everything.
There are several example programs for you to try. They are called fullmab.clp, zebra.clp, and wordgame.clp. fullmab.clp is the Monkey and Bananas problem featured at the Jess web site. To run it yourself from the command line, just type
java jess.Main examples/fullmab.clp (or examples\fullmab.clp on Win32)and the problem should run, producing a few screensfull of output. Any file of CLIPS code (given that it contains only CLIPS constructs and functions implemented by Jess, as described in this document) can be run this way. Note that giving Jess a file name on the command line is like using the 'batch' command in CLIPS; therefore, you need to make sure that the file ends with
(reset)
(run)
or nothing will happen. zebra.clp and wordgame.clp are
two other classic CLIPS examples, slightly modified to run under Jess.
Both of these examples were selected to show how Jess deals with tough
situations. These examples both generate huge numbers of partial pattern
matches, so they are slow and use up a lot of memory. They each take
about one second to run, depending on your computer.
Jess also has an interactive command-line interface, which has been improved for Jess 4.0. Just type java jess.Main to get a 'Jess>' prompt. In support of this, there is now an (exit) command. To execute a file of CLIPS code from the command prompt, use the 'batch' command:
Jess> (batch myfile.clp)
(lots of output)
Jess now sports a 'system' command, which means, for example, that
you can invoke an editor from the Jess command line to edit a file of Jess
code before reading it in with 'batch'. 'system' will also help to allow
non-Java programmers to integrate Jess with other applications. Given that
you have an editor named 'notepad' on your system, try
Jess> (system notepad README)
TRUE
I'm using an extremely informal notation to describe syntax. Basically strings in <angle-brackets> are some kind of data that must be supplied; things in [square brackets] are optional, and ellipses (...) are used to indicate one or more of the preceding. In general, input to Jess is free-format; newlines are generally not significant and are treated as whitespace.
In the example dialogs, you type what appears after the Jess> prompt. The system responds with the text in bold.
foo first-value contestant#1 _abc
3 4. 5.643
"foo" "Hello, World" "\"Nonsense\," he said firmly."
(+ 3 2) (a b c) ("Hello, World") () (deftemplate foo (slot bar))
The first element of a list (the 'car' of the list in LISP parlance) is
often called the list's 'head' in Jess.
; This is a list (a b c)
Function calls in Jess use a prefix notation. A list whose head is an atom that is the name of an existing function can be evaluated as an expression. For example, an expression that uses the "+" function to add the numbers 2 and 3 would be written (+ 2 3). When evaluated, the value of this expression is the number 5 (not a list containing the single element 5!) In general, expressions are recognized as such and evaluated in context when appropriate. You can type expressions at the Jess> prompt; Jess evaluates the expression and prints the result.
Jess> (+ 2 3)
5
Jess> (+ (+ 2 3) (* 3 3))
14
Note that arithmetic results may be returned as floating-point numbers
or as integers, depending on the types of the arguments.
Jess implements only a small subset of CLIPS intrinsic functions. These are functions which are essentially built into Jess and cannot be removed. All of these have been designed to function as much like their CLIPS counterparts as possible. The currently supported intrinsic functions are
*, +, -, /, <, <=, <>, =, >, >=, and, assert, assert-string,
bind, clear, close, eq, eq*, exit, facts, foreach, gensym*, get-var,
halt, if, jess-version-number, jess-version-string, load-facts,
mod, modify, neq, not, open, or, printout, read, readline, reset,
retract, return, rules, run, save-facts, sym-cat, undefrule,
unwatch, watch, while
On the other hand, I'm supplying implementations for many more CLIPS functions
as 'Userfunctions' - external functions written in Java that you can plug
into Jess. See the files jess/StringFunctions.java (string handling
functions: str-cat, str-compare, etc), jess/MultiFunctions.java
(multifield functions: create$, nth$), jess/PredFunctions.java
(predicates: oddp, stringp, etc), jess/MiscFunctions.java (batch,
system), and jess/MathFunctions.java (abs, sqrt) for more information.
All of the included Userfunctions are installed into the command-line version
of Jess by default; you can pick and choose in your own applications. In
applets, in particular, you may want to include only the Userfunctions
you need, to keep the size of the applet down. (see Extending Jess with Java for information
about doing this.)
Here is the complete list of Userfunctions shipped with Jess 4.0:
**, abs, bag, batch, build, call, complement$, create$, delete$,
definstance, defclass, div, e, engine, eval, evenp, exp, explode$, first$, float, floatp,
format, get, get-member, implode$, insert$, integer, integerp, intersection$,
length$, lexemep, list-function$, load-function, load-package,
log, log10, lowcase, max, member$, min, multifieldp, new, nth$,
numberp, oddp, pi, ppdefrule, random, replace$, rest$, round,
set, set-member, setgen, socket, sqrt, str-cat, str-compare, str-index,
str-length, stringp, sub-string, subseq$, subsetp, symbolp,
system, time, undefinstance, union$, upcase
All these functions are described in detail later
in this document.
(bind ?x "The value")Multifields are generally created using special multifield functions like <#createmf>create$#createmf>, and can then be boud to multivariables:
(bind $?grocery-list (create$ eggs bread milk))Variables need not (and cannot) be declared before their first use (except for Defglobals.)
(deffunction <function-name> [<doc-comment>] ([<parameter1> [<parameter2> [...]]]) [<expr1> [<expr2> [...]]] [<return-specifier>])The <function-name> must be an atom. Each parameter must be a variable name (all functions use pass-by-value semantics). The optional <doc-comment> is a double-quoted string that can describe the purpose of the function. The <expr> are an arbitrary number of arbitrary expressions. The optional <return-specifier> gives the return value of the function. It can either be an explicit use of the 'return' function, or it can be any value or expression. Control flow in deffunctions is achieved via the special control-flow expressions 'while' and 'if'. The following is a deffunction that returns the numerically larger of its two arguments:
(deffunction max (?a ?b)
(if (> ?a ?b) then
(return ?a)
else
(return ?b)))
(temperature 98.6)
(shopping-list bread milk paper-towels)
(start-processing)
Unordered facts are more structured; they contain a definite set of 'slots'
which must be accessed by name. While ordered facts can be used without
prior definition, unordered facts must be defined using the deftemplate
construct (see below, Deftemplates ).
Facts are placed on the fact-list by the 'assert' function. You can see the current fact list using the 'facts' function. You can remove a fact from the fact-list if you know its 'fact ID'. For example,
Jess> (assert (foo bar))
<Fact-0>
Jess> (facts)
f-0 (foo bar)
For a total of 1 facts.
TRUE
Jess> (retract 0)
TRUE
Jess> (facts)
For a total of 0 facts.
TRUE
(deftemplate <deftemplate-name> [<doc-comment>] (slot <slot-name> [(default <value>)] [(type <typespec>)]) [(slot ...) ...])The <deftemplate-name> is the head of the facts that will be created using this deftemplate. The <slot-name> must be an atom. The 'default' slot qualifier states that the default value of a slot in a new fact is given by <value>; the default is the atom 'nil'. The 'type' slot qualifier is accepted (for CLIPS compatibility) but ignored by Jess.
As an example, defining the following deftemplate
(deftemplate automobile
"A specific car."
(slot make)
(slot model)
(slot year)
(slot color (default white)))
would allow you to define facts like this:
Jess> (assert (automobile (make Chrysler) (model LeBaron) (year 1997)))
<Fact-0>
Jess> (facts)
f-0 (automobile (make Chrysler) (model LeBaron) (year 1997) (color white))
For a total of 1 facts.
TRUE
Note that the car is white, by default. Also note that any number of additional
automobiles could also be simultaneously asserted onto the fact list using
this deftemplate.
A given slot in a deftemplate fact can normally hold only one value. If you want a slot that can hold multiple values, use the 'multislot' keyword instead:
(deftemplate box
(slot location)
(multislot contents))
(assert (box (location kitchen) (contents spatula sponge frying-pan)))
(defclass pump jess.examples.pumps.Pump)The tag pump is just like a deftemplate name. Jess automatically builds a deftemplate from the named Java class, in this case jess.examples.pumps.Pump , an example class shipped with Jess. Jess turns Java Beans properties into deftemplate slots (if you're not familiar with Java Beans, this basically means that Jess will find any methods in the class named get<X> or is<X> which return non-void and take no arguments. Jess uses the BeanInfo mechanism to learn about Bean properties.) As an example, the jess.examples.pumps.Pump class looks something like this (lots cut out!):
public class Pump
{
public String getName() { ... }
public int getFlow() { ... }
public void setFlow(int f) { ... }
}
Jess will generate this deftemplate
(deftemplate pump "$JAVA-OBJECT$ jess.examples.pumps.Pump"
(slot class)
(slot name)
(slot flow)
(slot OBJECT))
The extra slot class comes from the getClass() method
inherited from java.lang.Object; and the OBJECT slot is added
to every defclass by Jess: it always holds a reference to the Java
object the matched pattern refers to. We're getting a bit ahead of
ourselves; we'll come back to object matching below when we talk about
the definstance construct.
There's a simple Bean class to experiment with in jess.examples.simple.
(deffacts <deffacts-name>
[<doc-comment>]
<fact1>
[...])
The deffacts-name is not used; its primary purpose is documentation. A
deffacts can contain any number of facts. Any unordered facts in a deffacts
must have previously been defined via a deftemplate construct when the
deffacts is parsed. The following is a valid deffacts construct:
(deffacts automobiles
(automobile (make Chrysler) (model LeBaron) (year 1997))
(automobile (make Ford) (model Contour) (year 1996))
(automobile (make Nash) (model Rambler) (year 1948)))
In any case, you can install an object for pattern matching like this
(definstance pump (new jess.examples.pumps.Pump "MAIN" ?tank))where the first argument to definstance is a defclass tag, and the second is an expression returning the Java object. Here we are creating the object using the new function (see below for a discussion of how to do this), but it instead could come from a variable, from the return value of a Userfunction, etc. "MAIN" and ?tank are arguments to the jess.examples.pumps.Pump constructor.
Once declared in a definstance construct, a Java object will be represented at all times by a single fact in Jess. This fact will be modified each time the object sends a PropertyChangeEvent. You can write patterns on the LHS of a rule to match this automatically generated and maintained fact, and as a result, you can match the state of the Java object. For example, given the pump defclass and definstance, we can write the following rule:
(defrule decrease-pump-flow-if-high-1
"If a pump's flow rate is over 20, decrease it by 5 units."
(pump (flow ?f&:(> ?f 20)) (OBJECT ?pump))
=>
(set ?pump flow (- ?f 5)))
Note that by binding the value of the OBJECT slot on the LHS, we can
modify the matched object from the RHS of the rule.
You can also (modify) function for this purpose:
(defrule decrease-pump-flow-if-high-2
"If a pump's flow rate is over 20, decrease it by 5 units."
?pump <- (pump (flow ?f&:(> ?f 20)))
=>
(modify ?pump (flow (- ?f 5))))
These two rules are equivalent; the latter may be slightly more efficient.
The Pump example given here is taken from a full, working example that ships with Jess. To try it out, compile the classes in jess/examples/pumps:
javac jess\examples\pumps\*.javaand then run the example
java jess.Main examples\pumps\pumps.clpRead the Java source files and the pumps.clp file to see what's going on. It's a real-time control problem, and Jess does a passable job of keeping two leaky tanks from overflowing or running dry. The Pumps and Tanks are Java objects that run in their own Threads, while Jess reacts to their PropertyChangeEvents, triggering rules which in turn adjust the Pumps.
Note: You must be careful not to trigger any
PropertyChangeEvents via calculations you perform on the LHS
of any rule. A rule of thumb is not to set any Java variables or call
any methods that might possible change an objects state; property
accessor methods are fine. Violation of this warning can cause Thread
deadlock in the engine.
2.15 Defrules
The main purpose of a shell like Jess is to support the execution of rules.
Rules in Jess are somewhat like the IF...THEN statements of other programming
languages; in operation, Jess constantly tests to see if any of the IFs
become true, and executes the corresponding THENs (actually, it doesn't
work quite this way, but this is a good way to imagine things. See
below, How Jess Works, for a more
truthful explanation.)
The 'intelligence' embedded in an intelligent rule-based system is encoded
in the rules. The defrule construct is used to define a rule to Jess.
(defrule <defrule-name>
[<doc-comment>]
[<salience-declaration>]
[[<pattern-binding> <- ] <pattern1>]
[ (more patterns) ]
=>
[<action1> [ <action2> ... ]])
Basically, a rule consists of a list of patterns (the IF part, on the
rule's left-hand-side (LHS)) and a list
of actions (the THEN part, on the rule's right-hand-side, or RHS). The patterns are matched against the fact list.
When facts are found that match all the patterns of a rule, the rule becomes
activated, meaning it may be fired (have its actions executed). An
activated rule may become deactivated before firing if the facts that
matched its patterns are retracted, or removed from the fact list,
while it is waiting to be fired. Here is an example of a simple rule:
(defrule example-1
"Announce 'a b c' facts"
(a b c)
=>
(printout t "Saw 'a b c'!" crlf))
To see this rule in action, enter it at the Jess> prompt, assert the fact
(a b c), then the (run) command to start the Jess engine. You'll get some
interesting additional information by first issuing the (watch all) command:
Jess> (clear)
TRUE
Jess> (watch all)
TRUE
Jess> (defrule example-1
"Announce 'a b c' facts"
(a b c)
=>
(printout t "Saw 'a b c'!" crlf))
example-1: +1+1+1+1+t
TRUE
Jess> (assert (a b c))
==> Activation: example-1 : f-0
==> (a b c)
<Fact-0>
Jess> (run)
FIRE [Defrule: example-1 "Announce 'a b c' facts";
1 patterns; salience: 0] f-0
Saw 'a b c'!
TRUE
Jess>
When you enter the rule, you see the sequence of symbols +1+1+1+1+t. This
tells you something about the way that Jess compiled the rule you wrote
into the internal rule representation. Then when you assert the fact, Jess
responds by telling you that the new fact was assigned the numeric fact
identifier 0 (f-0), and that it is an ordered fact with head 'a' and additional
fields 'b' and 'c'. Then it tells you that the rule example-1 is activated
by the fact f-0, that fact you just entered. When you type (run), you see
an indication that your rule has been fired, including a list of the relevant
fact IDs. The line "Saw 'a b c'!" is the result the execution of your rule.
Multiple activated rules are fired in order of salience (see below). Within a given salience value, the most recently activated rules will fire first (CLIPS' depth strategy, the default.)
If all the patterns of a rule had to be given literally as above, Jess would not be very powerful. Patterns can, however, also include wildcards and various kinds of predicates (comparisons and boolean functions). Firstly, you can specify a variable name instead of a value for a field in any of a rule's patterns (but not the pattern's head.) A variable matches any value in that position within a rule. For example, the rule
(defrule example-2
(a ?x ?y)
=>
(printout t "Saw 'a " ?x " " ?y "'" crlf))
will be activated each time any fact with head 'a' having two fields is
asserted: (a b c), (a 1 2), (a a a), etc. As in the example, the variables
thus matched in the patterns (or left-hand-side, LHS) of a rule are available
in the actions (right-hand-side, RHS) of the same rule.
Each such variable field in a pattern can also include any number of tests to qualify what it will match. Tests follow the variable name and are separated from it and from each other by ampersands. (The variable name itself is actually optional). Tests can be:
(defrule example-3
(not-b-and-c ?n1&~b ?n2&~c)
(different ?d1 ?d2&~?d1)
(same ?s ?s)
(more-than-one-hundred ?m&:(> ?m 100))
=>
(printout t "Found what I wanted!" crlf))
The first pattern will match a fact with head 'not-b-and-c' with exactly
two fields such that the first is not 'b' and the second is not 'c'. The
second pattern will match any fact with head 'different' and two fields
such that the two fields have different values. The third pattern will
match a fact with head 'same' and two fields with identical values. The
last pattern matches a fact with head 'more-than-one-hundred' and a single
field with a numeric value greater than 100.
A few more details about patterns: you can match a field without binding it to a variable by omitting the variable name and using just a question mark (?) as a placeholder. You can match any number of fields using a multivariable (one starting with $?):
Jess> (defrule example-4
(grocery-list $?list)
=>
(printout t "I need to buy " $?list crlf))
TRUE
Jess> (assert (grocery-list eggs milk bacon))
TRUE
Jess> (run)
I need to buy eggs milk bacon
TRUE
(defrule example-5
?fact <- (command "retract me")
=>
(retract ?fact))
The variable (?fact, in this case) is assigned the fact ID of the particular
fact that activated the rule.
(defrule example-6
(declare (salience -100))
(command exit-when-idle)
=>
(printout t "exiting..." crlf))
(This rule contains no patterns). Declaring a low salience value for a
rule makes it fire after all other rules of higher salience. A high value
makes a rule fire before all rules of lower salience. The default salience
value is zero.
(defrule example-7
(person ?x)
(not (married ?x))
=>
(printout t ?x " is not married!" crlf))
Note that a 'not' pattern cannot contain any variables that are not bound
before that pattern (since a 'not' pattern does not match any facts, it
cannot be used to define the values of any variables!) You can use
blank variables, however (a blank variable is a bare '?' or '$?'.)
A 'not' pattern can similarly not have a pattern binding.
(defrule example-8
(person (age ?x))
(test (> ?x 30))
=>
(printout t ?x " is over 30!" crlf))
Note that a 'test' pattern, like a 'not', cannot contain any variables
that are not bound before that pattern. 'Test' and 'not' may be combined:
(not (test (eq ?X 3)))
is equivalent to
(test (neq ?X 3))
2.16 Defglobals
Jess can support 'global variables' that are visible from the command-prompt
or inside any rule or deffunction. You can define them using the defglobal
construct:
(defglobal
<varname1> = <value1>
[<varname2> = <value2> [...]])
Note that defglobals are reset to their assigned values by the
(reset) command. Unlike CLIPS, the initialization value is
evaluated once, at compile-time, not each time a (reset) is issued.
(foo bar|baz)you can write
(foo ?x&:(or (eq ?X bar) (eq ?X baz)))to achieve the same effect in Jess.
The bag command does different things based on its first argument. It's really seven commands in one:
Jess> (bag create my-bag) <External-Address> Jess>
Jess> (defglobal ?*bag* = 0) TRUE Jess> (bind ?*bag* (bag create my-bag)) <External-Address> Jess> (bag set ?*bag* my-prop 3.0) 3.0 Jess> (bag get ?*bag* my-prop) 3.0
The functor 'call' may be omitted if the method being called is non-static and the object is represented by a simple variable. The following two method calls are equivalent:
;; These are legal and equivalent
(call ?vector addElement (new java.lang.String "Foo"))
(?vector addElement (new java.lang.String "Foo"))
'Call' may not be omitted if the object comes from the return value of
another function call:
;; This is illegal
((new java.lang.Vector 10) addElement (new java.lang.String "Foo"))
(if (> ?x 100)
then
(printout t "X is big" crlf)
else
(printout t "X is small" crlf))
Jess> (replace$ (create$ a b c) 2 2 (create$ x y z))
(a x y z c)
I've tried hard to improve Jess' syntax error reporting in this release, but it is still not as detailed as it could be. When you get an error from Jess (during parsing or at runtime) it is generally delivered as a Java exception. The exception will contain an explanation of the problem, and the stack trace of the exception will help you understand what went wrong. For example, if you attempt to load the folowing rule in Jess:
Jess> (defrule foo-1
(foo bar)
->
(printout "Found Foo Bar" crlf))
You'll get the following printout:
Rete Exception in routine Jesp::parseDefrule.
Message: Expected '=>' at line 3: ( defrule foo ( foo bar ) - .
at java.lang.Throwable.(Compiled Code)
at java.lang.Exception.(Compiled Code)
at jess.ReteException.(Compiled Code)
at jess.ParseException.(Compiled Code)
at jess.Jesp.parseError(Compiled Code)
at jess.Jesp.doParseDefrule(Compiled Code)
at jess.Jesp.parseDefrule(Compiled Code)
at jess.Jesp.parseSexp(Compiled Code)
at jess.Jesp.parse(Compiled Code)
at jess.Main.main(Compiled Code)
Looking at the routine names listed in the stack trace make it fairly
clear that a Defrule was being parsed, and the detail message explains
that the position of the '.' was reached in the input without finding
the expectyed '=> symbol (we accidentally typed '->' instead.)
Runtime errors can be more puzzling, but the stack trace will give you a lot of information. Here's a rule where we erroneously try to add the number 3.0 to the word 'four':
Jess> (defrule foo-2
=>
(printout t (+ 3.0 four) crlf))
When we (reset) and (run) we'll see:
Rete Exception in routine Value::numericValue.
Message: Not a number: four type = 1 at line 5: ( run ) .
at java.lang.Throwable.(Compiled Code)
at java.lang.Exception.(Compiled Code)
at jess.ReteException.(Compiled Code)
at jess.Value.numericValue(Compiled Code)
at jess._plus.call(Compiled Code)
at jess.Funcall.simpleExecute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Defrule.fire(Compiled Code)
at jess.Activation.fire(Compiled Code)
at jess.Rete.run(Compiled Code)
at jess.Rete.run(Compiled Code)
at jess._run.call(Compiled Code)
at jess.Funcall.simpleExecute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Jesp.parseSexp(Compiled Code)
at jess.Jesp.parse(Compiled Code)
at jess.Main.main(Compiled Code)
In this case, the error message is pretty clear, except for the claim
that the offending statement is 'run'. To find out what was really
happening, we have to look at the stack trace. Starting from the top
down, we find Value.numericValue() was called by
_plus.call(). A few levels down, we see
Defrule.fire(). Taken together, this means that an addition
operation on the RHS of a rule found the symbol four as one
of its operands, when it wanted a number.
The notation 'type = 1' in the error message, by the way, refers to a set of constants in the class jess.RU. The values of these constants are given in a later Section of this document. Consulting that table, we see that type 1 is RU.ATOM, a symbol, which is indeed not a number.
If we make a similar mistake on the LHS of a rule:
Jess> (defrule foo-3
(test (eq 3 (+ 2 one)))
=>
)
We see the following after a (reset):
Rete Exception in routine Value::numericValue.
Message: Not a number: one type = 1 at line 17: ( reset ) .
at java.lang.Throwable.(Compiled Code)
at java.lang.Exception.(Compiled Code)
at jess.ReteException.(Compiled Code)
at jess.Value.numericValue(Compiled Code)
at jess._plus.call(Compiled Code)
at jess.Funcall.simpleExecute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.NodeTest.runTests(Compiled Code)
at jess.NodeTest.callNode(Compiled Code)
at jess.Node.passAlong(Compiled Code)
at jess.Node1TELN.callNode(Compiled Code)
at jess.Node.passAlong(Compiled Code)
at jess.Node1TECT.callNode(Compiled Code)
at jess.Rete.processTokenOneNode(Compiled Code)
at jess.Rete.processToken(Compiled Code)
at jess.Rete.assert(Compiled Code)
at jess.Rete.reset(Compiled Code)
at jess._reset.call(Compiled Code)
at jess.Funcall.simpleExecute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Funcall.execute(Compiled Code)
at jess.Jesp.parseSexp(Compiled Code)
at jess.Jesp.parse(Compiled Code)
at jess.Main.main(Compiled Code)
Again, the error message is somewhat but not completely helpful, and
the stack trace contains additional information. Here we see our old
friends Value.numericValue() and _plus.call() being
called, but we don't see the Defrule being fired; instead wee see lots
of oddly named classes and functions with names containing
Node and Token. This is always a tip-off that the
error happened on Defrule LHS processing. Way down the stack we see
Rete.assert() being called by Rete.reset(), which
also indicates that LHS processing was in progress when the exception
happened.
import jess.*;
// ...
// See info about the ReteDisplay classes below
NullDisplay nd = new NullDisplay();
// Create a Jess engine
Rete rete = new Rete(nd);
// Open the file test.clp
FileInputStream fis = new FileInputStream("test.clp");
// Create a parser for the file, telling it where to take input
// from and which engine to send the results to
Jesp j = new Jesp(fis, rete);
try
{
// parse and execute the code, without printing a prompt
j.parse(false);
}
catch (ReteException re)
{
// All Jess errors are reported as 'ReteException's.
re.printStackTrace(nd.stderr());
}
Note that if the file 'test.clp' contains the CLIPS (reset) and (run) commands,
the Jess engine will run to completion during the parse() call. Also note
that Jess will throw jess.ReteException exceptions to signal errors.
try
{
rete.executeCommand("(reset)");
rete.executeCommand("(assert (foo bar foo))");
rete.executeCommand("(run)");
}
catch (ReteException ex)
{
System.err.println("Foo bar error.");
}
Commands executed via executeCommand() may refer to Jess
variables; they will be interpreted in the global context. In
general, only defglobals can be used in this
way.
There is a set of diagnostic methods in Rete which return Enumerations of various data structures in the engine:
public interface ReteDisplay
{
// Rete sets its initial input/output using these
// These must be implemented
public java.io.PrintStream stdout();
public java.io.InputStream stdin();
public java.io.PrintStream stderr();
// These notify the ReteDisplay object when things happen
// Can do nothing if you want!
public void assertFact(ValueVector fact);
public void retractFact(ValueVector fact);
public void addDeffacts(Deffacts df);
public void addDeftemplate(Deftemplate dt);
public void addDefrule(Defrule rule);
public void activateRule(Defrule rule);
public void deactivateRule(Defrule rule);
public void fireRule(Defrule rule);
// This gives the Rete object access to Applet resources
// Should return null if not in an Applet
public java.applet.Applet applet();
}
Jess ships with three different classes that implement this
interface. You can write your own, or modify these, for use in your
own applications.
class MyDisplay
{
public TextArea ta = new TextArea(20, 80);
TextAreaOutputTream taos = new TextAreaOutputStream(ta);
PrintStream ps = new PrintStream(taos, true);
MyDisplay()
{
Frame f = new Frame("Jess Demo");
f.add(ta);
f.pack();
f.show();
}
PrintStream stdout() { return ps; }
// ...
Now if you construct a jess.Rete object with an instance of
this display, the Jess output will go into a scrolling window. Study
jess/Main.java and jess/LostDisplay.java to see a complete example of
this.
jess.TextInputStream is similar, but it is an InputStream instead. It is actually quite similar to java.io.StringBufferInputStream, except that you can continually add new text to the 'end' of the stream (using the appendText() method.) It is intended that you create a jess.TextInputStream, return it from your jess.ReteDisplay.stdin() method, and then (in an AWT event handler, somewhere) append new input to the stream whenever it becomes available. See jess/QuizDisplay.java for a complete usage example.
5.3 Manipulating Jess I/O Routers in Java
ReteDisplay lets you set up the minumal initial state for a Rete object, but Jess can read from more that just standard input and standard output. Jess I/O routers can be easily manipulated from Java. There are six functions in the Rete class that manipulate the router list:
The Rete class has two more handy router-related methods: outStream() and errStream(), both of which return a PrintStream object. outStream returns a stream that goes to the same place as the current setting of WSTDOUT; errStream() does the same for WSTDERR.
You can add your own routers which do I/O through any Java streams; they will immediately be usable from Jess. Look at the implementation of the socket Userfunction in jess/MiscFunctions.java for an idea of what's possible.
final public static int NONE = 0; ; an empty value (not NIL) final public static int ATOM = 1; ; a symbol final public static int STRING = 2; ; a string final public static int INTEGER = 4; ; an integer final public static int VARIABLE = 8; ; a variable final public static int FACT_ID = 16; ; a fact index final public static int FLOAT = 32; ; a double float final public static int FUNCALL = 64; ; a function call final public static int ORDERED_FACT = 128; ; an ordered fact final public static int UNORDERED_FACT = 256; ; a deftemplate fact final public static int LIST = 512; ; a multifield final public static int DESCRIPTOR = 1024; ; (internal use) final public static int EXTERNAL_ADDRESS = 2048; ; a Java object final public static int INTARRAY = 4096; ; (internal use) final public static int MULTIVARIABLE = 8192; ; a multivariable final public static int SLOT = 16384; ; (internal use) final public static int MULTISLOT = 32768; ; (internal use)Value objects are constructed by specifying the data and the type. Each overloaded constructor assures that the given data and the given type are compatible. Note that for each constructor, more than one value of the type parameter is acceptable. The available constructors are:
public Value(Object o, int type) throws ReteException public Value(String s, int type) throws ReteException public Value(Value v) public Value(ValueVector f, int type) throws ReteException public Value(double d, int type) throws ReteException public Value(int value, int type) throws ReteException public Value(int[] a, int type) throws ReteExceptionValue supports a number of functions to get the actual data out of a Valueobject. These are
public Object externalAddressValue() throws ReteException public String stringValue() throws ReteException public ValueVector factValue() throws ReteException public ValueVector funcallValue() throws ReteException public ValueVector listValue() throws ReteException public double floatValue() throws ReteException public double numericValue() throws ReteException public int atomValue() throws ReteException public int descriptorValue() throws ReteException public int factIDValue() throws ReteException public int intValue() throws ReteException public int variableValue() throws ReteException public int[] intArrayValue() throws ReteExceptionIf you try to convert random values by creating a Value and retrieving it as some other type, you'll generally get a ReteException. However, many types can be freely interconverted: Strings and atoms, for example, or integers and floats.
Note that Jess stores all strings, atoms and variable names as integers, which are used as indexes into a hashtable. Thus if Value.type() returns RU.ATOM or RU.STRING, you can call either atomValue() (which returns that integer) or stringValue() (which returns a Java String object.) To convert a String to an appropriate integer, call the static function int RU.putAtom(String). To get the String that goes with an integer, call String RU.getAtom(int). Note that this is NOT a way to convert the String "1" to the integer 1; it converts Strings into unique hash codes.
Facts (type RU.ORDERED_FACT or RU.UNORDERED_FACT) are stored as a ValueVector with the slots filled in a special way, as follows (the constants representing slot numbers MUST be used, as they may change)
| SLOT NUMBER | TYPE | DESCRIPTION |
| RU.CLASS | RU.ATOM | The 'head' or first field of the fact |
| RU.ID | RU.FACT_ID | The fact-id of this fact |
| RU.DESC | RU.DESCRIPTOR | One of RU.ORDERED_FACT or RU.UNORDERED_FACT |
| RU.FIRST_SLOT | (ANY) | Value of the first slot of this fact |
| RU.FIRST_SLOT + 1 | (ANY) | second |
| ... | ... | ... |
Function calls (RU.FUNCALLs) are simpler; the first slot is the functor as an RU.ATOM, and all remaining slots are arguments. And multifields are even simpler: each element of the ValueVector is an element of the multifield.
Rete engine = ...;
Foo foo = new Foo();
// Define the defglobal
engine.executeCommand("(defglobal ?*x* = 0)");
// Create the Funcall object
Funcall f = new Funcall("bind", engine);
// Now add the arguments, in left-to-right order. Each argument
// is a jess.Value object. Note the use of the
// RU.EXTERNAL_ADDRESS type to pass in the Java object
f.add(new Value(RU.putAtom("*x*"), RU.VARIABLE));
f.add(new Value(foo, RU.EXTERNAL_ADDRESS));
// Now execute the function (simpleExecute is a static function.)
f.simpleExecute(f, rete.globalContext());
// Now Jess code has access to the Java object!
engine.executeCommand("(printout t ?*x* crlf)");
There are a few restrictions on what you can do with this technique:
Jess maintains separate mutexes for RHS execution and for LHS execution; in other words, only one assert or retract may be going on in a single engine at once, and only one rule RHS may be executing at once; however, these two activities may occur simultaneously.
If you use Java object matching on rule LHSs, be aware of interactions between multiple threads. It is certainly possible to come up with combinations of objects and rule engines that will deadlock; the easiest way to do this is to trigger a PropertyChangeEvent from a rule LHS, so be careful not to do that!
The Java interface jess.Userfunction represents a single user-supplied function, while the interface jess.Userpackage represents a whole set of such functions. When you write a new function for Jess, you do it by writing a class that implements the jess.Userfunction interface (see below for details on how this is done.) Then a single instance of this class is created and installed into Jess. These objects can maintain state and can be retrieved by other code you write (see below.) Therefore a Userfunction can cache results across invocations, maintain complex data structures, or keep references to external Java objects for callbacks.
rete.addUserfunction(new MyFunction());
or an entire package of such functions in a class 'mypackage' using
rete.addUserpackage(new MyPackage());
You can also load extension functions and packages from the Jess language
itself. The equivalents to the above are
(load-function "myfunction")
and
(load-package "mypackage")
Note that if the new classes or user packages come in a Java package, you'll
need to specify the fully qualified name of the class:
(load-package "xyzzy.bassomatic.mypackage")In any case, the relevant classes need to be reachable on your Java CLASSPATH.
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: jess.Value, jess.ValueVector, and jess.Funcall, as discussed in the previous Section.
To implement the jess.Userfunction interface, you need to implement only two methods: name() 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 MyUpcase implements Userfunction
{
// The name method returns the integer representation of the name.
// This function will be called by Jess.
public int name() { return RU.putAtom("my-upcase"); }
public Value call(ValueVector vv, Context context) throws ReteException
{
return new Value(vv.get(1).stringValue().toUpperCase(), RU.STRING);
}
}
The name() function returns an integer that represents the
name of the function. You must obtain the appropriate integer using
the static function int RU.putAtom(String s). name()
almost always looks exactly like this.
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 call was made,
(my-upcase "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.ATOM) "my-upcase", and the second argument would
be a Value containing the string (RU.STRING) "foo".
Note that we use vv.get(1).stringValue() 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, a ReteException may be thrown that describes 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); vv.get(1) is the first argument, and vv.get(2) would be the second, if this function accepted multiple arguments. If you want, you can check how many arguments your function was called with and throw a ReteException 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).
Having written this class, you can then, in your mainline code, simply call Rete.addUserfunction() with an instance of your new class as an argument, and the function will be available from Jess code. Adding to our mainline code from the last section:
// Add the 'my-upcase' command to Jess
rete.addUserfunction(new MyUpcase());
// Exceute some Jess code that calls this function
rete.executeCommand("(printout t (my-upcase foo) crlf)");
will print "FOO".
public class StringFunctions implements Userpackage
{
public void add(Rete engine)
{
engine.addUserfunction(new strcat());
engine.addUserfunction(new upcase());
engine.addUserfunction(new lowcase());
engine.addUserfunction(new strcompare());
engine.addUserfunction(new strlength());
engine.addUserfunction(new substring());
}
}
Now in your mainline, you can call
engine.addUserpackage(new jess.StringFunctions());
and from your Jess code, you can call str-cat, str-compare, 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.
There are a lot of small Userfunction classes in the jess package
which you can use as examples for writing your own Jess
extensions. All of these small classes can add a lot of size overhead
to Applets, which is why they are not all built-in to the
engine. These days, with zips and JAR files, this isn't such a big
deal. Still, you can leave them out if you want just by not adding the
relevant Userpackage from your mainline program. Contrarily, if you
don't add these packages to your programs, the corresponding Jess
functions will not be available.
6.5 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 most useful
when you write Userfunctions which themselves maintain state of some
kind, and you need access to that state.
This new capability makes Jess more than just an expert system shell; it is now also a dynamic, extensible, portable Java-based scripting environment.
7.1 Creating Java Objects
The Jess new function lets you create Java objects. The first
argument is the fully-qualified name of the class as a symbol or
String; any remaining arguments are passed to the Java object's
constructor. For example:
(new java.lang.StringBuffer 100)
will create an instance of StringBuffer, passing the integer
100 as a constructor argument.
In the case of overloaded constructors, the constructor will be chosen from among all constuctors for the named class with the given number of arguments based on a "first-best fit" algorithm. Built-in Jess types are converted as necessary to match the available constructors. The Jess atoms TRUE and FALSE are automatically converted to Java booleans, while the atom nil is automatically converted to the Java null pointer. Jess multifields are automatically converted to one-dimensional Java arrays; there is no way to represent a multidimensional Java array in Jess (if this becomes necessary, you can always write a Userfunction to call the appropriate constructor.) Floating point values are converted to the best-matching floating-point type. External address types are unwrapped and passed directly as Java arguments. If you have trouble calling the correct constructor, you can often disambiguate between multiple constructors by using Java wrapper objects. For example, if the imaginary Foo class had the two constructors
Foo(double d);
Foo(float f);
you could specifically call the one that takes a float argument
instead of the double argument like this:
way:
(new Foo (new java.lang.Float 123.456))
An example: to use the void java.lang.StringBuffer.append(String s) method directly, you can write
(defglobal ?*str-buf* = (new java.lang.StringBuffer 100))
(call ?*str-buf* append "Some String Data To Append")
Note that in many cases, explicit use of the call functor is
optional; it can be omitted in function calls that are not nested
inside of other function calls. For example, the above call to append
could also be written as
(?*str-buf* append "Some String Data To Append")
A static method example: you can invoke the Java garbage collector
using the java.lang.System.gc() method like this:
(call java.lang.System gc)
An example: if ?pt holds a java.awt.Point object, you can reference its x coordinate field like this:
(printout t "The value of x is " (get-member ?pt x))
An example: AWT components have many Bean properties. One is visible, the property of being visible on the screen. We can query this property in two ways: either by explicitly calling the isVisible() method, or by querying the Bean property.
(defglobal ?*frame* (new java.awt.Frame "Frame Demo"))
; Directly call 'isVisible', or...
(printout t (call ?*frame* isVisible) crlf)
; ... equivalently, query the Bean property
(printout t (get ?*frame visible) crlf)
It should now be obvious that you can easily construct GUI objects from Jess; for example, here is a Button:
(defglobal ?*b* = (new java.awt.Button "Hello"))
What should not be obvious is how, from Jess, you can arrange to have
somthing happen whan the button is pressed. For this, I have provided
a full set of EventListener classes:
An example should clarify matters. Let's say that when the "Hello" button is pressed, you would like the string "Hello, World!" to be printed to standard output (how original!) What you need to do is:
(defglobal ?*f* = (new java.awt.Frame "Button Demo"))
(defglobal ?*b* = (new java.awt.Button "Hello"))
(deffunction say-hello (?evt)
(printout t "Hello, World!" crlf))
(?*b* addActionListener
(new jess.reflect.ActionListener say-hello (engine)))
(?*f* add ?*b*)
(?*f* pack)
(set ?*f* visible TRUE)
The Jess engine function returns the jess.Rete
object in which it is executed, as an external address. You'll have to
quit using ^C. To fix this, you can add a WindowListener which handles
WINDOW_CLOSING events to the above program:
(deffunction frame-handler (?evt)
(if (= (?evt getID) (get-member ?evt WINDOW_CLOSING)) then
(call (get ?evt source) dispose)
(exit)))
(?*f* addWindowListener
(new jess.reflect.WindowListener frame-handler (engine)))
Now when you close the window Jess will exit. Notice how we can examin
the ?evt parameter for event information.
See the demo examples/frame.clp for a slightly more complex example of how you can build an entire Java graphical interface from within Jess.
(pi)and
(get-member java.lang.Math PI)both return the value of the static final member PI of the class java.lang.Math, but the first call is much more efficient because (pi) is a Userfunction. If you are going to call a Java function just once, go ahead and use call, but if you're going to use it in a loop, consider wrapping it in a Userfunction if performance might be an issue.
Jess is a rule-based expert system shell. In the simplest terms, this means that Jess's purpose it to continuously apply a set of if-then statements, called rules, to a set of data, called the fact list. You define the rules that make up your own particular expert system. Rules in Jess look something like this:
(defrule library-rule-1
(book (name ?X) (status late) (borrower ?Y))
(borrower (name ?Y) (address ?Z))
=>
(send-late-notice ?X ?Y ?Z))
Note that this syntax is borrowed from (and is identical to) the syntax
used by CLIPS. This rule might be translated into psueudo-english like
this:
Library rule #1:
If
a late book exists, with name X, borrowed by someone named Y
and
that borrower's address is known to be Z
then
send a late notice to Y at Z about the book X.
The book and borrower entities would be found on the fact list. The fact
list is therefore a kind of database of bits of factual knowledge about
the world. The attributes (called "slots") that things like books and borrowers
are allowed to have are defined in statements called "deftemplates"; actions
like send-late-notice can be defined in user-written functions in the Jess
language ("deffunctions") or in Java ("Userfunctions.") For more information
about the CLIPS rule syntax (and to work with Jess, you will certainly
need to learn more!) refer to the previous Section, and possibly to the
CLIPS documentation as mentioned above.
In a typical expert system a fixed set of rules is used, but the fact list changes continuously. However, it is an empirical fact that in most expert systems, much of the fact list is also fairly fixed; although new facts are arriving and old ones being removed at all times, the percentage of facts that change per unit time is generally fairly small. For this reason, the obvious implementation for the expert system shell is a very inefficient one. This obvious implementation would be to keep a list of the rules, and continuously cycle through the list, checking each one's left-hand-side (LHS) against the fact list, and executing the right-hand-side (RHS) of any rules that apply. This is inefficient because most of the tests made on each cycle will have the same results as on the previous iteration; since the fact list is stable, most of the tests will be repeated. You might call this the 'rules finding facts' approach, and the computational complexity is of the order of O(RPF), where R is the number of rules, P is the average number of patterns per rule LHS, and F is the number of facts on the fact list. This is effectively n^2 in the size of the system.
Jess instead uses a very efficient method known as the Rete (Latin for "net") algorithm. The classic paper on the Rete algorithm ("Rete: A Fast Algorithm for the Many Pattern/ Many Object Pattern Match Problem", Charles L. Forgy, Artificial Intelligence 19(1982), 17-37) became the basis for a whole generation of fast expert system shells: OPS5, its descendant ART, and CLIPS. In the Rete algorithm, the inefficiency described above is alleviated (conceptually) by remembering past test results across iterations of the rule loop. Only new facts are tested against any rule LHSs. Additionally, as will be described below, new facts are tested against only the rule LHSs to which they are most likely to be relevant. As a result, the computational complexity per iteration drops to something more like O(sqrt(RP)). Our discussion of the Rete algorithm is necessarily brief; the interested reader is referred to the Forgy paper or to Giarrantano and Riley, "Expert Systems: Principles and Programming", Second Edition, PWS Publishing (Boston, 1993) for a more detailed treatment.
The Rete algorithm is implemented by building a network of nodes, each of which represents one or more tests found on a rule LHS. Facts that are being added to or removed from the fact list are processed by this network of nodes. At the bottom of the network are nodes representing individual rules; when a set of facts filters all the way down to the bottom of the network, it has passed all the tests on the LHS of a particular rule and this set becomes an "activation"; the associated rule may have its RHS executed ("be fired") if the activation is not invalidated first by the removal of one or more facts from its activation set.
Within the network itself there are broadly two kinds of nodes: one-input and two-input nodes. One-input nodes perform tests on individual facts, while two-input nodes perform tests across facts and perform the grouping function. Subtypes of these two classes of node are also used, and there are also auxilliary types such as the terminal nodes mentioned above.
An example is often useful at this point. The following rules:
(defrule example-2 (defrule example-3
(x) (x)
(y) (y)
(z) => )
=> )
might be compiled into the following network:
+----+ +----+ +----+ +----+ +----+ (one-input nodes)
| x? | | y? | | z? | | x? | | y? |
+----+ +----+ +----+ +----+ +----+
\ / | \ /
+------------+ | +------------+
| + | | | + |
+------------+ | +------------+
\ | | (two-input nodes)
+------------+ |
| + | |
+------------+ |
| |
+----------------+ +----------------+
| fire example-2 | | fire example-3 | (terminals)
+----------------+ +----------------+
The nodes marked x?, etc., test if a fact contains the given data,
while the nodes marked + remember all facts and fire whenever they've
received data from both their left and right inputs. To run the network,
Jess presents new facts to each node at the top of the network as they
added to the fact list. Each node takes input from the top and sends its
output downwards. A single input node generally receives a fact from above,
applies a test to it, and, if the test passes, sends the fact downward
to the next node. If the test fails, the one-input nodes simply do nothing.
The two-input nodes have to integrate facts from their left and right inputs,
and in support of this, their behavior must be more complex. First, note
that any facts that reach the top of a two-input node could potentially
contribute to an activation: they pass all tests that can be applied to
single facts. The two input nodes therefore must remember all facts that
are presented to them, and attempt to group facts arriving on their left
inputs with facts arriving on their right inputs to make up complete activation
sets. A two-input node therefore has a 'left memory' and a 'right memory'.
It is here in these memories that the inefficiency described above is avoided.
A convenient distinction is to divide the network into two logical components:
the single-input nodes comprise the "pattern network", while the two-input
nodes make up the "join network".
There are two simple optimizations that can make Rete even better, The first is to share nodes in the pattern network. In the network above, there are five nodes across the top, although only three are distinct. We can modify the network to share these nodes across the two rules (the arrows coming out of the top of the x? and y? nodes are outputs):
+--------------------------+
^ +-------------+ |
| ^ | |
+----+ +----+ +----+ | |
| x? | | y? | | z? | | |
+----+ +----+ +----+ | |
/ / / | |
+------------+ / +---/ +------------+
| + |-+ / | + |
+------------+ / +------------+
\ / |
+------------+ |
| + | |
+------------+ |
| |
+----------------+ +----------------+
| fire example-2 | | fire example-3 |
+----------------+ +----------------+
But that's not all the redundancy in the original network. Now we see that
there is one join node that is performing exactly the same function (integrating
x,y pairs) in both rules, and we can share that also:
+----+ +----+ +----+
| x? | | y? | | z? |
+----+ +----+ +----+
/ / /
+------------+ / +---/
| + |-+ /
+------------+ /
| \ /
| +------------+
| | + |
| +------------+
| |
| +----------------+
| | fire example-2 |
| +----------------+
+----------------+
| fire example-3 |
+----------------+
The pattern and join networks are collectively only half the size they
were originally; this kind of sharing comes up very frequently in real
systems, and is a significant performance booster!
You can see the amount of sharing in a Jess network by using the 'watch compilations' command. When a rule is compiled and this command has been previously executed, Jess prints a string of characters something like this, which is the actual output from compiling rule example-2, above:
example-2: +1+1+1+1+1+1+2+2+tEach time '+1' appears in this string, a new one-input node is created; +2 indicates a new two-input node. Now watch what happens when we compile example-3:
example-3: =1=1=1=1=2+tHere we see that =1 is printed whenever a preexisting one-input node is shared; =2 is printed when a two-input node is shared. +t represents the terminal nodes being created. (Note that the number of single-input nodes is larger than expected; Jess creates separate nodes that test for the head of each pattern and its length, rather than doing both of these tests in one node, as we implicitly do in our graphical example.) No new nodes are created for rule example-3; Jess shares existing nodes very efficiently in this case.
Jess's Rete implementation is very literal. Different types of network nodes are represented by various subclasses of the Java class jess.Node: Node1, Node2, NodeNot2, NodeTest, and NodeTerm. The Node1 class is further specialized because it contains a 'command' member which causes it to act differently depending on the tests or functions it needs to perform. For example, there are specializations of Node1 which test the first field (called the 'head') of a fact, test the number of fields of a fact, test single slots within a fact, and compare two slots within a fact. There are further variations which participate in the handling of multifields and multislots. The Jess language code is parsed by the class jess.Jesp, while the actual network is assembled by code in the class jess.ReteCompiler. The execution of the network is handled by the class Rete. The jess.Main class itself is really just a small demonstration driver for the jess package, in which all of the interesting work is done.
The 'view' command, distributed for the first time with Jess 3.2, is a graphical viewer for the Rete network itself; I have used this as a debugging tool for Jess, but it may have educational value for others, and it may help you to design more efficient systems of rules in Jess. Issuing the 'view' command after entering the rules example-2 and example-3 produces a very good facsimile of the drawing (although it correctly shows the larger number of one-input nodes.) The various nodes are color-coded according to their roles in the network; Node1 nodes are red; Node2 nodes are green; NodeNot2 nodes are yellow; and NodeTerm nodes are blue. Passing the mouse over a node displays information about the node and the tests it contains; double-clicking on a node brings up a dialog box containing the same information (for join nodes, the memory contents are also displayed, while for NodeTerm nodes, a pretty-print representation of the the rule is shown.) See the description of the view function for important information before using it.
At the time of this writing, Jess has more than 5000 registered users. I have been very pleased by this response and have enjoyed working with many of Jess's more ambitious users. If you use Jess, and if you have comments, questions, or concerns, please don't hesitate to ask.
Finally, thanks to Gary Riley and the gang at NASA for writing the marvelous CLIPS in the first place!
Ernest Friedman-Hill Phone: (510) 294-2154 Distributed Computing FAX: (510) 294-2234 Sandia National Labs ejfried@ca.sandia.gov Org. 8920, MS 9214 http://herzberg.ca.sandia.gov PO Box 969 Livermore, CA 94550