Distribution
Category UC-411
Ernest J. Friedman-Hill
Distributed Computing Systems
Sandia National Laboratories
Livermore, CA
Version 3.2 (November 8, 1997)
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 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.
This is the 3.2 final release. Nevertheless, there may well 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-3.2.tar.Z tar xf Jess-3.2.tarIf 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 'Jess32'. Inside this directory should be the following files:
README.html | This file |
TextAreaOutputStream.java Jess.java LostDisplay.java NullDisplay.java |
Java source files. Jess.java implements both the applet interface and the command-line interface. NullDisplay is used by the command-line version; the other two are used in the applet's GUI. |
jess/ | A directory containing the 'jess' package. There are many source files in here that implement Jess's inference engine. |
examples/ | A directory of tiny example CLIPS 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: the commands
javac *.java jess/*.java (UNIX)or
javac *.java 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 file Jess.java exists is on your CLASSPATH; this may mean including '.' (dot). 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 is one optional source file in the subdirectory Jess32/jess/view/. This file defines the optional debugging command 'view'. It can be compiled only with Java 1.1 or later.
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 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 may each take tens of seconds to run, depending on your computer, but they will run.
Jess now has an interactive command-line interface, which has been improved for Jess 3.1. Just type java Jess 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 also 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) TRUEThe class 'Jess', which contains the main routine that allows you to execute CLIPS code from the command line, also implements an Applet interface, so that it will run in a Web browser. The Applet interface is specialized to run only the 'mab.clp' Monkey and Banana example. To create your own graphical applets using the Jess classes, read on, and check out the Sections about calling Jess from Java and vice-versa. You can modify the Jess class, or you can write your own from scratch (which is probably a better idea.)
Jess supports the following basic data types: SYMBOL, STRING, INTEGER, FLOAT, FACT_ID, and EXTERNAL_ADDRESS. In addition, values of the following types are used internally by Jess: VARIABLE, FUNCALL, ORDERED_FACT, UNORDERED_FACT, LIST, DESCRIPTOR, INTARRAY, and NONE. Note that all Jess numbers obtained via scanning textual input become FLOATs. INTEGERs may be returned by functions, however.
Because of the way Jess compiles rules, it works much better if you define your rules first before loading in facts. Deffacts don't count as loading in facts, but the (reset) and (assert) commands do. Put your rules first in your Jess input files for the most efficient operation of Jess.
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, eq, exit, facts, gensym*, halt, if, jess-version-number, jess-version-string, load-facts, mod, modify, neq, not, 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 Section 8, Extending Jess with Java, for information about doing this.)
Here is the complete list of Userfunctions shipped with Jess 3.2:
**, abs, batch, complement$, create$, delete$, div, e, evenp, exp, first$, float, floatp, implode$, insert$, integer, integerp, intersection$, length$, lexemep, load-function, load-package, log, log10, lowcase, max, member$, min, multifieldp, nth$, numberp, oddp, pi, random, replace$, rest$, round, setgen, sqrt, str-cat, str-compare, str-index, str-length, stringp, sub-string, subseq$, subsetp, sym-cat, symbolp, system, time, union$, upcase .All these functions are described in detail later in this document.
One more note - in the interest of size and speed, Jess assumes that your input code is largely correct CLIPS code. As a result, if your CLIPS code is syntactically invalid, Jess's error messages may be less than helpful. It certainly would help you develop Jess code if you had a copy of CLIPS to test on.
I'm using an extremely informal notation here 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.0 Jess> (+ (+ 2 3) (* 3 3)) 14.0Note that all arithmetic results are returned as floating-point numbers; all arithmetic is done as floating-point by Jess. A comprehensive list of functions implemented in Jess, with descriptions, is given in Section 4.
(bind ?x "The value")Variables need not (and cannot) be declared before their first use.
(deffunction <function-name> ([<parameter1> [<parameter2> [...]]]) [<doc-comment>] [<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 Section 3.11, 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) [Fact: foo (ordered) 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-1> Jess> (facts) [Fact: automobile (unordered) make=Chrysler; model=LeBaron; year=1997; color=white;] For a total of 1 facts. TRUENote 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)))
(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)))
(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) and a list of actions (the THEN part.) 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) as soon as any other activated rules have been fired. 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)) ==> f-0 [Fact: a (ordered) b c] ==> Activation: example-1 : f-0 <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.
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. Tests can either be a literal value (in which case the variable matches only that value,) another variable (which must have been matched earlier in the rule LHS), one of the previous two options preceded by a tilde (~), in which case the test is for inequality, or a colon (:) followed by a function call, in which case the test succeeds if the function returns the special value TRUE (actually in Java it must return a Value object which compares equal to that returned by the static function jess.Funcall.TRUE()). This means you can use any internal or user-defined boolean function as a test. Popular ones are things like eq and neq (comparison) and integerp and stringp (type testing). You can use nested function calls as well; i.e., do arithmetic, then compare the result to a fixed value using eq. Here's an example of a rule that uses several kinds of tests.
(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!) A 'not' pattern can similarly not have a pattern binding.
(defglobal <varname1> = <value1> [<varname2> = <value2> [...]])Note that defglobals, like deffacts, have no effect until a 'reset' command is issued.
(foo bar|baz)you can write
(foo ?X&:(or (eq ?X bar) (eq ?X baz)))to achieve the same effect in Jess.
(foo ?X ?Y) (test (eq ?X (+ ?Y 3)))can be translated into
(foo ?X ?Y&:(eq ?X (+ ?Y 3))).
(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)
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 (Greek 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 ancestor 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, 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 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 (unless they've been 'deactivated' via the undefrule command or being redefined, in which case they are invisible. )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. See the description of the view function for important information before using it.
// See info about the Display classes in Section 7 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 all the classes in the Jess package will throw ReteException exceptions to signal errors.
For more control over Jess from your Java program, you can use the Rete.ExecuteCommand(String) method. For example, after the above code, you could include the following:
try { rete.ExecuteCommand("(reset)"); rete.ExecuteCommand("(assert (foo bar foo))"); rete.ExecuteCommand("(run)"); } catch (ReteException ex) { System.err.println("Foo bar error."); }I made an effort to make Jess 'sort-of' threadsafe. Sort-of is not nearly good enough, however, so be careful how you use Jess in a multithreaded application. One major difference between Jess and CLIPS is that you can call (run) from a rule RHS and have a new rule fired up in the middle of RHS execution! This should be used very carefully, if at all.
Jess provides an interface 'ReteDisplay' that provides hooks into the engine's internal workings. The Display provides two types of functions: functions that return an input, output, and error stream to the engine, and functions that are called by the engine whenever an event occurs (events here meaning a construct is parsed, fact is asserted, rule is activated, etc.) You can implement ReteDisplay as a simple way of providing a GUI for your Jess application. The fancy multicolored display for the Jess home page is implemented in the class LostDisplay. Writing a ReteDisplay is the simplest way to customize Jess. See Section 7 for more info about ReteDisplay.
See the file Jess.java for ideas on how to implement Jess
applets and applications.
7 Capturing and Processing Jess Output
One simple way to use CLIPS as part of a larger system is to capture
and process what CLIPS prints on its standard output. This is also
possible in Jess. This Section explains how to use the ReteDisplay
class to do this.
This is actually very easy to do, but maybe hard to describe. Jess stdin and stdout are Java streams. Jess gets these streams by calling the methods ReteDisplay.stdin() and stdout(). To capture Jess's output, then, all you need to do is to subclass ReteDisplay so that it returns a custom output stream that captures and processes the information in some way. There is an example of this in the Jess distribution, as described below.
Observe that the Jess 'monkey' applet's output appears in a scrolling window. How is this done? The scrolling window is created using an instance of the class TextAreaOutputStream (part of Jess.) TextAreaOutputStream is a subclass of OutputStream that implements all the OutputStream methods such that the data appears in a TextArea Component.
In the distributed 'monkey' applet, Jess.init() creates a TextArea widget, then a TextAreaOutputStream using that widget, then a PrintStream from the TextAreaOutputStream. It then uses this PrintStream to construct a LostDisplay object (a subclass of ReteDisplay) and then uses that LostDisplay to construct a Rete engine. As a result, the output from the Rete engine ends up in the TextArea. (The name LostDisplay comes from the fact that the graphics remind me of the credits from the old TV show 'Lost in Space'.)
So, to capture and process the printed output from Jess, you need to:
The class jess.Userfunction represents a single user-supplied function, while the class jess.Userpackage represents a whole set of such functions. Given that you have written some classes which inherit from these classes, you can load these extensions into Jess in two ways. First, you can load them in from Java code. Given that 'rete' is the Rete object in your application, and 'myfunction' is the name of a Userfunction class you (or someone else!) wrote, you can add the new function to Jess by calling
rete.AddUserfunction(new myfunction());or an entire package of such functions in a class 'mypackage' using
rete.AddUserpackage(new mypackage());Starting in Jess 3.1, you can load functions and packages form 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 two basic Jess data structures: class Value and class ValueVector. Look at the source for these classes if the following discussion isn't clear.
final public static int NONE = 0; final public static int ATOM = 1; final public static int STRING = 2; final public static int INTEGER = 4; final public static int VARIABLE = 8; final public static int FACT_ID = 16; final public static int FLOAT = 32; final public static int FUNCALL = 64; final public static int ORDERED_FACT = 128; final public static int UNORDERED_FACT = 256; final public static int LIST = 512; final public static int DESCRIPTOR = 1024; final public static int EXTERNAL_ADDRESS = 2048; final public static int INTARRAY = 4096; final public static int MULTIVARIABLE = 8192; final public static int SLOT = 16384; final public static int MULTISLOT = 32768;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 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 |
... | ... | ... |
Note that for ordered facts, the slots are stored in the order in which they appear, but in unordered (deftemplate) facts, they appear in the order given in the corresponding deftemplate.
Function calls (RU.FUNCALLs) are simpler; the first slot is the functor as an RU.ATOM, and all remaining slots are arguments. When your user function is called, the first argument to the Java Call function will be a ValueVector representation of the Jess code that evoked your function.
Now, on to writing a user function. First, create a class that implements the interface jess.Userfunction, which just contains the two methods name() and Call(). A listing is worth a 1000 words:
// A user function that implements the CLIPS 'upcase' operation in // terms of the java.lang.String.toUpperCase() method. class MyUpcase implements jess.Userfunction { private int _name = RU.putAtom("upcase"); // The name method returns the integer representation of the name. // This function will be called by Jess. public int name() { return _name; } public Value Call(ValueVector vv, Context context) throws ReteException { return new Value(vv.get(1).StringValue().toUpperCase(), RU.STRING); } }Note that we use vv.get(1).StringValue() to get the first argument to 'upcase' as a java String. If the argument doesn't contain a string, a ReteException will be thrown that describes the problem; hence you don't need to worry about incorrect argument types. vv.get(0) will always return 'upcase', the name of the function being called. vv.get(1) is the first argument, 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.
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 'upcase' command to Jess rete.AddUserfunction(new upcase()); // Exceute some Jess code that calls this function rete.ExecuteCommand("(printout t (upcase foo) crlf)");will print "FOO".
Jess 3.0 added the jess.Userpackage interface. jess.Userpackage is a handy way to group a collection of Userfunctions together. A Userpackage class should supply the one method Add(), which adds a collection of Userfunctions to a Rete engine using AddUserfunction(). Nothing mysterious going on, but it's very convenient. Implementations for strcat, strcompare, etc, are found in the sample file jess/StringFunctions.java.
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 StringFunctions());and from your Jess code, you can call str-cat, str-compare, etc.
There are a lot of new small classes in the Jess package which serve as examples of Userfunctions. 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 removing the line that adds the relevant Userpackage from your mainline program.
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