2.
The Jess Language
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, things ending with +
can appear one or more times, and things ending with * can appear
zero or more times.
In general, input to Jess is free-format. Newlines are generally not
significant and are treated as whitespace; exceptions will be noted.
2.1. Basics
2.1.1. Atoms
The atom or symbol is a core concept of the Jess language. Atoms are very
much like identifiers in other languages. A Jess atom can contain letters,
numbers, and the following punctuation: $*=+/<>_?#. . An atom
may not begin with a number; it may begin with some punctuation marks (some
have special meanings as operators when they appear at the start of an
atom). The best atoms consist of letters, numbers, underscores, and dashes;
dashes are traditional word separators. The following are all valid atoms:
foo first-value contestant#1 _abc
There are three "magic" atoms that Jess interprets specially:
nil, which is somewhat akin to Java's null value;
and TRUE and FALSE, which are Jess' boolean values.
2.1.2. Numbers
Jess parses only simple floating point and integer numbers. It does not
accept scientific or engineering notation. The following are all valid
numbers:
3 4. 5.643
2.1.3. Strings
Character strings in Jess are denoted using double quotes
("). Backslashes (\) can be used to
escape embedded quote symbols. Note that Jess strings are unlike Java
strings in several important ways. First, no "escape sequences" are
recognized. You cannot embed a newline in a string using
"\n", for example. On the other hand, real newlines are
allowed inside double-quoted strings; they become part of the string.
The following are all valid strings:
"foo" "Hello, World" "\"Nonsense,\" he said firmly." "Hello,
There"
The last string is equivalent to the Java string "Hello,\nThere".
2.1.4. Lists
Another fundamental unit of syntax in Jess is the list. A list always consists
of an enclosing set of parentheses and zero or more atoms, numbers, strings,
or other lists. The following are valid lists:
(+ 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.
2.1.5. Comments
Programmer's comments in Jess begin with a semicolon (;) and extend
to the end of the line of text. Here is an example of a comment:
; This is a list
(a b c)
Comments can appear anywhere in a Jess program.
2.2. Functions
As in LISP, all code in Jess (control structures, assignments,
procedure calls) takes the form of a function call.
Function calls in Jess are simply lists. Function calls use a prefix
notation; a list whose head is an
atom that is the name of an existing function can be a function call.
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 you can nest function calls; the outer function is
responsible for evaluating the inner function calls.
Jess comes with a large number of built-in functions that do
everything from math, program control and string manipulations, to
giving you access to Java APIs.
One of the most commonly used functions is
printout
.
printout
is used to send text to Jess's
standard output, or to a file. A complete explanation will have to
wait, but for now, all you need to know is contained in the following
example:
Jess> (printout t "The answer is " 42 "!" crlf)
The answer is 42!
Another useful function is
batch
.
batch
evaluates a
file of Jess code. To run the Jess source file
examples/hello.clp you can enter
Jess> (batch examples/hello.clp)
Hello, world!
Each of these functions (along with all the others) is described more
thoroughly in the Jess function guide.
2.3. Variables
Programming variables in Jess are atoms that begin with the question mark
(?) character. The question mark is part of the variable's name.
A normal variable can refer to a single atom, number, or string. A variable
whose first character is instead a $ (for example, $?X)
is a multivariable, which can refer to a special kind of list
called a multifield. You assign to any variable using the
bind
function:
Jess> (bind ?x "The value")
"The value"
Multifields are generally created using special multifield functions like
create$
and can then be bound to multivariables:
Jess> (bind $?grocery-list (create$ eggs bread milk))
(eggs bread milk)
Variables need not (and cannot) be declared before their first use (except
for special variables called defglobals).
Note that to see the value of a variable at the Jess> prompt, you can
simply type the variable's name.
Jess> (bind ?a 123)
123
Jess> ?a
123
2.3.1. Global variables (or defglobals)
Any variables you create at the Jess> prompt, or at the "top level" of
any Jess language program, are cleared whenever the
reset
command is issued. This makes them somewhat transient; they are fine
for scratch variables but are not persistent global variables in the
normal sense of the word. To create global variables that are not
destroyed by
reset
, you can use the defglobal
construct.
(defglobal [?<global-name> = <value>]+)
Global variable names must begin and end with an asterisk. Valid
global variable names look like
?*a* ?*all-values* ?*counter*
When a global variable is created, it is initialized to the given
value. When the
reset
command is subsequently issued, the
variable may be reset to this same value, depending on the
current setting of the reset-globals property. There is a
function named
set-reset-globals
that you can use to set this
property. An example will help.
Jess> (defglobal ?*x* = 3)
TRUE
Jess> ?*x*
3
Jess> (bind ?*x* 4)
4
Jess> ?*x*
4
Jess> (reset)
TRUE
Jess> ?*x*
3
Jess> (bind ?*x* 4)
4
Jess> (set-reset-globals nil)
FALSE
Jess> (reset)
TRUE
Jess> ?*x*
4
You can read about the
set-reset-globals
and the accompanying
get-reset-globals
function in the Jess function guide.
2.4. Deffunctions
You can define your own functions using the deffunction
construct. A deffunction construct looks like this:
(deffunction <function-name> [<doc-comment>] (<parameter>*)
<expr>*
[<return-specifier>])
The <function-name> must be an atom. Each <parameter>
must be a variable name. The optional <doc-comment> is a
double-quoted string that can describe the purpose of the
function. There may be an arbitrary number of <expr>
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 control-flow
functions like
foreach
,
if
,
and
while
. The
following is a deffunction that returns the larger of its two
numeric arguments:
Jess> (deffunction max (?a ?b)
(if (> ?a ?b) then
(return ?a)
else
(return ?b)))
TRUE
Note that this could have also been written as:
Jess> (deffunction max (?a ?b)
(if (> ?a ?b) then
?a
else
?b))
TRUE
This function can now be called anywhere a Jess function call can be
used. For example
Jess> (printout t "The greater of 3 and 5 is " (max 3 5) "." crlf)
The greater of 3 and 5 is 5.
Normally a deffunction takes a specific number of
arguments. To write a deffunction that takes an arbitrary
number of arguments, make the last formal parameter be a multifield
variable. When the deffunction is called, this multifield
will contain all the remaining arguments passed to the function. A
deffunction can accept no more than one such wildcard
argument, and it must be the last argument to the function.
2.5. Defadvice
Sometimes a Jess function won't behave exactly as you'd like. The
defadvice
construct lets you write some Jess code which will
be executed before or after each time a given Jess function is
called.
defadvice
lets you easily "wrap" extra code
around any Jess function, such that it executes before (and thus can
alter the argument list seen by the real function, or short-circuit it
completely by returning a value of its own) or after the real function
(and thus can see the return value of the real function and possibly
alter it. ) defadvice provides a great way for Jess add-on authors to
extend Jess without needing to change any internal code.
Here are some examples of what defadvice looks like.
This intercepts calls to 'plus' (+) and adds the extra argument '1',
such that (+ 2 2) becomes (+ 2 2 1) -> 5. The variable '$?argv' is
special. It always refers to the list of arguments the real Jess
function will receive when it is called.
Jess> (defadvice before + (bind $?argv (create$ $?argv 1)))
TRUE
Jess> (+ 2 2)
5
This makes all additions equal to 1. By returning, the defadvice keeps
the real function from ever being called.
Jess> (defadvice before + (return 1))
TRUE
Jess> (+ 2 2)
1
This subtracts one from the return value of the + function. ?retval is
another magic variable - it's the value the real function
returned. When we're done, we remove the advice with
undefadvice
.
Jess> (defadvice after + (return (- ?retval 1)))
TRUE
Jess> (+ 2 2)
3
Jess> (undefadvice +)
Jess> (+ 2 2)
4
2.6. Java reflection
Among the list of functions above are a set that let you create and
manipulate Java objects directly from Jess. Using them, you can do
virtually anything you can do from Java code, except for defining new
classes. Here is an example in which I create a Java Hashtable
and add a few String objects to it, then lookup one object and display it.
Jess> (bind ?ht (new java.util.Hashtable))
<External-Address:java.util.Hashtable>
Jess> (call ?ht put "key1" "element1")
Jess> (call ?ht put "key2" "element2")
Jess> (call ?ht get "key1")
"element1"
As you can see, Jess converts freely between Java and Jess types when it can. Java
objects that can't be represented as a Jess type are called external
address values. The Hashtable in the example above is one
of these.
Jess can also access member variables of Java objects using the
set-member
and
get-member
functions.
Jess> (bind ?pt (new java.awt.Point))
<External-Address:java.awt.Point>
Jess> (set-member ?pt x 37)
37
Jess> (set-member ?pt y 42)
42
Jess> (get-member ?pt x)
37
You can access static members by using the name of the class instead
of an object as the first argument to these functions.
Jess> (get-member System out)
<External-Address:java.io.PrintStream>
Note that we don't have to say "java.lang.System." The java.lang
package is implicitly "imported" much as it is in Java code. Jess also has an
import
function that you can use explicitly.
Jess converts values from Java to Jess types according to the following table.
Java type |
Jess type |
A null reference |
The atom 'nil' |
A void return value |
The atom 'nil' |
String |
RU.STRING |
An array |
A Jess multifield |
boolean or java.lang.Boolean |
The atoms 'TRUE' and 'FALSE' |
byte, short, int, or their wrappers |
RU.INTEGER |
long or Long |
RU.LONG |
double, float or their wrappers |
RU.FLOAT |
char or java.lang.Character |
RU.ATOM |
anything else |
RU.EXTERNAL_ADDRESS |
Jess converts values from Jess to Java types with some flexibility,
according to this table. Generally when converting in this direction,
Jess has some idea of a target type; i.e., Jess has a
java.lang.Class object and a jess.Value object, and
wants to turn the Value's contents into something assignable
to the type named by the Class. Hence the atom 'TRUE' could
be passed to a function expecting a boolean argument, or to one
expecting a String argument, and the call would succeed in both cases.
Jess type |
Possible Java types |
RU.EXTERNAL_ADDRESS |
The wrapped object |
The atom 'nil' |
A null reference |
The atoms 'TRUE' or 'FALSE' |
java.lang.Boolean or boolean |
RU.ATOM, RU.STRING |
String, char, java.lang.Character |
RU.FLOAT |
float, double, and their wrappers |
RU.INTEGER |
long, short, int, byte, char, and their wrappers |
RU.LONG |
long, short, int, byte, char, and their wrappers |
RU.LIST |
A Java array |
Sometimes you might have trouble calling overloaded methods - for
example, passing the String "TRUE" to a Java method that is overloaded
to take either a boolean or a String. In this case, you can always
resort to using an explicit wrapper class - in this case, passing a
java.lang.Boolean object should fix the problem.
To learn more about the syntax of
call
,
new
,
set-member
,
get-member
, and other Java integration functions, see the Jess function guide.
2.7. The knowledge base
A rule-based system maintains a collection of knowledge nuggets called
facts. This collection is known as the knowledge base.
It is somewhat akin to a relational database, especially in that the
facts must have a specific structure. In Jess, there are three kinds
of facts: ordered facts, unordered facts, and
definstance facts.
2.7.1. Ordered facts
Ordered facts are simply lists, where the first field (the head
of the list) acts as a sort of category for the fact. Here are some
examples of ordered facts:
(shopping-list eggs milk bread)
(person "Bob Smith" Male 35)
(father-of danielle ejfried)
You can add ordered facts to the knowledge base using the
assert
function. You can see a list of all the facts in the
knowledge base using the
facts
command. You can completely
clear Jess of all facts and other data using the
clear
command.
Jess> (reset)
TRUE
Jess> (assert (father-of danielle ejfried))
<Fact-1>
Jess> (facts)
f-0 (initial-fact)
f-1 (father-of danielle ejfried)
For a total of 2 facts.
As you can see, each fact is assigned an integer index (the
fact-id) when it is asserted. You can remove an individual fact
from the knowledge base using the
retract
function.
Jess> (retract (fact-id 1))
TRUE
Jess> (facts)
f-0 (initial-fact)
For a total of 1 facts.
The fact (initial-fact) is asserted by the
reset
command. It is used internally by Jess to keep track of its own
operations; you should generally not retract it.
2.7.2. Unordered facts
Ordered facts are useful, but they are unstructured. Sometimes (most
of the time) you need a bit more organization. In object-oriented
languages, object have named fields in which data
appears. Unordered facts offer this capability (although the fields are
traditionally called slots.)
(person (name "Bob Smith") (age 34) (gender Male))
(automobile (make Ford) (model Explorer) (year 1999))
before you can create unordered facts, you have to define the slots
they have using the deftemplate construct:
(deftemplate <deftemplate-name> [extends <classname>] [<doc-comment>]
[(slot <slot-name> [(default | default-dynamic <value>)]
[(type <typespec>))]*)
The <deftemplate-name> is the head of the facts that will
be created using this deftemplate. There may be an arbitrary
number of slots. Each <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 'default-dynamic' version will evaluate the
given value each time a new fact using this template is asserted. The
'type' slot qualifier is accepted but not currently enforced by Jess;
it specifies what data type the slot is allowed to hold. Acceptable
values are ANY, INTEGER, FLOAT, NUMBER, ATOM, STRING, LEXEME, and
OBJECT.
As an example, defining the following deftemplate:
Jess> (deftemplate automobile
"A specific car."
(slot make)
(slot model)
(slot year (type INTEGER))
(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.
Note that the car is white by default. If you don't supply a default
value for a slot, and then don't supply a value when a fact is
asserted, the special value nil is used. 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:
Jess> (deftemplate box (slot location) (multislot contents))
TRUE
Jess> (bind ?id (assert (box (location kitchen)
(contents spatula sponge frying-pan))))
<Fact-1>
(We're saving the fact-id returned by (assert) in the variable ?id,
for use below.) A multislot has the default value () (the empty list) if no other
default is specified.
You can change the values in the slots of an unordered fact using the
modify
command. Building on the immediately preceding
example, we can move the box into the dining room:
Jess> (modify ?id (location dining-room))
<Fact-1>
Jess> (facts)
f-0 (automobile (make Chrysler) (model LeBaron)
(year 1997) (color white))
f-1 (box (location dining-room)
(contents spatula sponge frying-pan))
For a total of 2 facts.
The optional extends clause of the deftemplate
construct lets you define one deftemplate in terms of
another. For example, you could define a used-auto as a kind of automobile
with more data:
Jess> (deftemplate used-auto extends automobile
(slot mileage)
(slot blue-book-value)
(multislot owners))
TRUE
A used-auto fact would now have all the slots of an automobile, plus
three more. As we'll see later, this inheritance relationship will let
you act on all automobiles (used or not) when you so desire, or only
on the used ones.
Note that an ordered fact is very similar to an unordered fact with
only one multislot. The similariety is so strong, that
in fact this is how ordered facts are implemented in Jess. If you
assert an unordered fact, Jess automatically generates a deftemplate
for it. This generated deftemplate will contain a single slot named
"__data". Jess treats these facts specially - the name of the slot is
normally hidden when the facts are displayed. This is really just a
syntactic shorthand, though; ordered facts really are just unordered
facts with a single multislot named "__data".
2.7.3. The deffacts construct
Typing separate
assert
commands for each of many facts is
rather tedious. To make life easier in this regard, Jess includes the
deffacts construct. A deffacts construct is a simply
a named list of facts. The facts in all defined deffacts are
asserted into the knowledge base whenever a
reset
command is
issued:
Jess> (deffacts my-facts "The documentation string"
(foo bar)
(box (location garage) (contents scissors paper rock))
(used-auto (year 1992) (make Saturn) (model SL1)
(mileage 120000) (blue-book-value 3500)
(owners ejfried)))
TRUE
Jess> (reset)
TRUE
Jess> (facts)
f-0 (initial-fact)
f-1 (foo bar)
f-2 (box (location garage) (contents scissors paper rock))
f-3 (used-auto (make Saturn) (model SL1) (year 1992)
(color white) (mileage 120000)
(blue-book-value 3500) (owners ejfried))
For a total of 4 facts.
Note that we can specify the slots of an unordered fact in any order
(hence the name.) Jess rearranges our inputs into a canonical
order so that they're always the same.
2.7.4. Definstance facts
You may have noticed that unordered facts look a bit like Java
objects, or specifically, like Java Beans. The similarity is that both
have a list of slots (for Java Beans, they're called
properties) which contains values that might change over
time. Jess has a mechanism for automatically generating deftemplates
that represent specific types of Java Beans. Jess can then use these
deftemplates to store a representation of a Java Bean's properties on
the knowledge base. The knowledge base representation of the Bean can
be static (changing infrequently, like a snapshot of the
properties at one point in time) or dynamic (changing automatically
whenever the Bean's properties change.) The Jess commands that make
this possible are
defclass
and
definstance.
defclass
tells Jess to generate a special
deftemplate to represent a category of Beans, while
definstance
puts a representation of one specific Bean onto
the fact base.
An example will probably help at this point. Let's say you have the
following Java Bean class
public class ExampleBean
{
private String m_name = "Bob";
public String getName() { return m_name; }
public void setName(String s) { m_name = s; }
}
This Bean has one property called "name". Before we can insert any of
these Beans onto the knowledge base, we need a deftemplate to
represent them: we must use
defclass
to tell Jess to generate
it:
Jess> (defclass simple ExampleBean)
ExampleBean
Jess> (ppdeftemplate simple)
(deftemplate simple extends __fact "$JAVA-OBJECT$ ExampleBean"
(slot class (default <External-Address:jess.SerializablePropertyDescriptor>))
(slot name (default <External-Address:jess.SerializablePropertyDescriptor>))
(slot OBJECT (type 2048)))
This is a strange looking deftemplate, but it does have a slot called
"name", as we'd expect, that arises from the "name" property of our
Bean. The slot "class" comes from the method getClass() that
every object inherits from java.lang.Object, while the slot
OBJECT is added by Jess; its value is always a reference to the Bean
itself. See how the first argument to
defclass
is used as the
deftemplate name.
Now let's say we want an actual ExampleBean in our knowledge base. Here
we'll create one from Jess code, but it could come from anywhere. We
will use the
definstance
function to add the object to the
knowledge base.
Jess> (bind ?sb (new ExampleBean))
<External-Address:ExampleBean>
Jess> (definstance simple ?sb static)
<Fact-0>
Jess> (facts)
f-0 (simple (class <External-Address:java.lang.Class>)
(name "Bob")
(OBJECT <External-Address:ExampleBean>))
For a total of 1 facts.
As soon as we issue the
definstance
command, a fact
representing the Bean appears in the knowledge base. Now watch what
happens if we change the "name" property of our Bean.
Jess> (call ?sb setName "Fred")
Jess> (facts)
f-0 (simple (class <External-Address:java.lang.Class>)
(name "Bob")
(OBJECT <External-Address:ExampleBean>))
For a total of 1 facts.
Hmmm. The knowledge base still thinks our Bean's name is "Bob", even
though we changed it to "Fred". What happens if we issue a
reset
command?
Jess> (reset)
TRUE
Jess> (facts)
f-0 (initial-fact)
f-1 (simple (class <External-Address:java.lang.Class>)
(name "Fred")
(OBJECT <External-Address:ExampleBean>))
For a total of 2 facts.
reset
updates the definstance facts in the knowledge base to
match their Java Beans. This behaviour is what you get when (as we did
here) you specify static in the
definstance
command. Static definstances are refreshed only when a reset is
issued.
If you want to have your definstance facts stay continuously up to
date, Jess needs to be notified whenever a Bean property changes. For
this to happen, the Bean has to support the use of
java.beans.PropertyChangeListeners. For Beans that fulfil this
requirement, you can specify dynamic in the definstance
command, and the knowledge base will be updated every time a property
of the Bean changes. Jess comes with some example Beans that can be
used in this way; see, for example, the
Jess60a1/jess/examples/simple directory.
defclass
es, like deftemplates, can extend one
another. In fact, deftemplates can extend defclasses, and defclasses
can extend deftemplates. Of course, for a
defclass
to extend a
deftemplate, the corresponding Bean class must have property names
that match the deftemplate's slot names. Note, also, that just because
two Java classes have an inheritance relationship doesn't mean that if
both are
defclass
ed the two
defclass
es will. You
must explicitly declare all such relationships using extends.
See the full documenation for
defclass
for details.
2.8. Rules and Queries
Now that we've learned how to develop a knowledge base, we can answer
the obvious question: what is it good for? The answer is that
queries can search it to find
relationships between facts, and rules
can take actions based on the contents of one or more facts.
2.8.1. Defrules
A Jess rule is something like an if... then statement
in a procedural language, but it is not used in a procedural
way. While if... then statements are executed at a specific
time and in a specific order, according to how the programmer writes
them, Jess rules are executed whenever their if parts (their
left-hand-sides or LHSs) are satisfied, given only that
the rule engine is running. This makes Jess rules less deterministic
than a typical procedural program. See the chapter on the Rete algorithm for an explanation of why this
architecture can be many orders of magnitude faster than an equivalent
set of traditional if... then statements.
Rules are defined in Jess using the defrule construct. A very
simple rule looks like this:
Jess> (defrule do-change-baby
"If baby is wet, change baby's diaper."
(baby-is-wet)
=>
(change-baby))
This rule has two parts, separated by the "=>" symbol (which you can
read as "then".) The first part consists of the LHS pattern
(baby-is-wet). The second part consists of the RHS
action (change-baby). Although it's hard to tell due
to the LISP-like syntax, the LHS of a rule consists of patterns which
are used to match facts in the knowledge base, while the RHS contains
function calls.
The LHS of a rule (the "if" part) consists of patterns that match
facts, NOT function calls. The actions of a rule (the "then"
clause) are made up of function calls. The following rule does
NOT work:
Jess> (defrule wrong-rule
(eq 1 1)
=>
(printout t "Just as I thought, 1 == 1!" crlf))
This rule will NOT fire just because the function call (eq 1 1)
would evaluate to true. Instead, Jess will try to find a fact on the
knowledge base that looks like (eq 1 1). Unless you have previously
asserted such a fact, this rule will NOT be activated and will
not fire. If you want to fire a rule based on the evaluation of a
function, you can use the test CE.
Our example rule, then, will be activated when the fact
(baby-is-wet) appears in the knowledge base. When the rule
executes, or fires, the function (change-baby) is
called (presumably this function is defined elsewhere in our imaginary
program.) Let's turn this rule into a complete program. The function
watch all tells Jess to print some useful diagnostics as we
enter our program.
Jess> (watch all)
TRUE
Jess> (reset)
==> f-0 (initial-fact)
TRUE
Jess> (deffunction change-baby () (printout t "Baby is now dry" crlf))
TRUE
Jess> (defrule do-change-baby
(baby-is-wet)
=>
(change-baby))
do-change-baby: +1+1+1+t
TRUE
Jess> (assert (baby-is-wet))
==> f-1 (baby-is-wet)
==> Activation: do-change-baby : f-1
<Fact-1>
Some of these diagnostics are interesting. We see first of all how
issuing the
reset
command asserts the fact
(initial-fact). You should always issue a
reset
command when working with rules. When the rule itself is entered, we
see the line "+1+1+t". This tells you something about how the rule is
interpreted by Jess internally (see The Rete
Algorithm for more information.) When the fact
(baby-is-wet) is asserted, we see the diagnostic "Activation:
do-change-baby : f-1". This means that Jess has noticed that the rule
do-change-baby has all of its LHS conditions met by the given
list of facts ("f-1").
After all this, our rule didn't fire; why not? Jess rules only fire
while the rule engine is running (although they can be
activated while the engine is not running.) To start the engine
running, we issue the
run
command.
Jess> (run)
FIRE 1 do-change-baby f-1
Baby is now dry
1
As soon as we enter the
run
command, the activated rule
fires. Since we have watch all, Jess prints the diagnostic
FIRE 1 do-change-baby f-1 to notify us of this. We then see the
output of the rule's RHS actions. The final number "1" is the number of
rules that fired (it is the return value of the
run
command.)
The
run
function returns when there are no more activated
rules to fire.
Rules are uniquely identified by their name. If a rule named
my-rule exists, and you define another rule named
my-rule, the first version is deleted and will not fire
again, even if it was activated at the time the new version was
defined.
2.8.1.1. Basic Patterns
If all the patterns of a rule had to be given literally as above, Jess
would not be very powerful. However, patterns can also include wildcards
and various kinds of predicates (comparisons and boolean functions).
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:
Jess> (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), and
so forth. As in the example, the variables thus matched in the patterns
(or LHS) of a rule are available in the actions (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 (&) or pipes
(|). (The variable name itself is actually optional.) Tests can be:
-
A literal value (in which case the variable matches only that
value); for example, the values b and c in (a b
c).
-
Another variable (which must have been matched earlier in the rule's LHS).
This will constrain the field to contain the same value as the variable
was first bound to; for example, (a ?X ?X) will only match "a"
facts followed by two equal values.
-
A colon (:) followed by a function call, in which case the test
succeeds if the function returns the special value TRUE. These
are called predicate constraints; for example, (a ?X&:(> ?X 10)
matches "a" facts with one field, a number greater than 10.
-
An equals sign (=) followed by a function call. In this case the
field must match the return value of the function call. These are called
return value constraints. Note that both predicate constraints and
return-value constraints can refer to variables bound elsewhere in this
or any preceding pattern in the same defrule.
Note: pretty-printing
a rule containing a return value contstraint will show that it has been
transformed into an equivalent predicate constraint. An example of a
return-value constraint would be (a ?X =(+ ?X 1)), which matches "a"
facts with two fields, both numbers with the second number greater than
the first by one.
-
Any of the other options preceded by a tilde (~), in which case
the sense of the test is reversed (inequality or false); for example
(a ?X ~?X) matches "a" facts with two fields as long as the
two fields contains different values.
Ampersands (&) represent logical "and", while pipes (|) represent
logical "or." & has a higher precedence than |, so that the
following
(foo ?X&:(oddp ?X)&:(< ?X 100)|0)
matches a foo fact with a single field containing either an odd
number less than 100, or 0.
Here's an example of a rule that uses several kinds of tests:
Jess> (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))
(red-or-blue red|blue)
=>
(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 fourth pattern matches a fact with head more-than-one-hundred
and a single field with a numeric value greater than 100. The last
pattern matches a fact with head red-or-blue followed by
either the atom red or the atom blue.
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
in a multislot or unordered fact 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))
<Fact-0>
Jess> (run)
I need to buy (eggs milk bacon)
1
If you match to a defglobal with a pattern like (foo ?*x*), the
match will only consider the value of the defglobal when the fact is
asserted. Subsequent changes to the defglobal's value will not
invalidate the match - i.e., the match does not reflect the current
value of the defglobal, but only the value at the time the matching
fact was asserted.
2.8.1.2. Pattern bindings
Sometimes you need a handle to an actual fact that helped to activate a
rule. For example, when the rule fires, you may need to retract or modify
the fact. To do this, you use a pattern-binding variable:
Jess> (defrule example-5
?fact <- (a "retract me")
=>
(retract ?fact))
The variable (?fact, in this case) is assigned the fact ID of
the particular fact that activated the rule.
Note that ?fact is a jess.Value object of type
RU.FACT, not an integer. It is basically a reference to a jess.Fact object. You can convert
an ordinary number into a FACT using the
fact-id
function. You can convert a FACT into an integer when necessary by
using reflection to call the Fact.getFactId() function. The
jess.Value.externalAddressValue() method can be called on a
FACT Value to obtain the actual jess.Fact object from Java
code. In Jess code, a fact-id essentially is a
jess.Fact, and you can call jess.Fact methods on a
fact-id directly:
Jess> (defrule example-5-1
?fact <- (initial-fact)
=>
(printout t (call ?fact getName) crlf))
TRUE
Jess> (reset)
TRUE
Jess> (run)
initial-fact
1
See the section on the
jess.FactIDValue class for more information.
2.8.1.3. Salience and conflict resolution
Each rule has a property called salience that is a kind of rule
priority. Activated rules of the highest salience will fire first,
followed by rules of lower salience. To force certain rules to always
fire first or last, rules can include a salience declaration:
Jess> (defrule example-6
(declare (salience -100))
(command exit-when-idle)
=>
(printout t "exiting..." crlf))
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. Salience values
can be integers, global variables, or function calls. See the
set-salience-evaluation
command for details about when such function calls will be evaluated.
The order in which multiple rules of the same salience are fired is
determined by the active conflict resolution strategy. Jess
comes with two strategies: "depth" (the default) and "breadth." In the
"depth" strategy, the most recently activated rules will fire before
others of the same salience. In the "breadth" strategy, rules fire in
the order in which they are activated. In many situations, the
difference does not matter, but for some problems the conflict
resolution strategy is important. You can write your own strategies in
Java; see the chapter on extending Jess with
Java for details. You can set the current strategy with the
set-strategy
command.
Note that the use of salience is generally discouraged, for two
reasons: first it is considered bad style in rule-based programming to
try to force rules to fire in a particular order. Secondly, use of
salience will have a negative impact on performance, at least with the
built-in conflict resolution strategies.
You can see the list of activated, but not yet fired, rules with the
agenda
command.
2.8.1.4. The 'or' conditional element.
Sometimes a number of rules will have identical right-hand sides, but two
different left-hand-sides:
Jess> (defrule i-like-food
(food ?x)
=>
(printout t "I like " ?x crlf))
Jess> (defrule i-like-beverages
(beverage ?x)
=>
(printout t "I like " ?x crlf))
Jess> (defrule i-like-animals
(animal ?x)
=>
(printout t "I like " ?x crlf))
It would be convenient to be able to combine these two rules into
one. You can use the or conditional element (CE) to do this:
Jess> (defrule i-like-everything
(or (food ?x)
(beverage ?x)
(animal ?x))
=>
(printout t "I like " ?x crlf))
Jess> (assert (food lobster) (beverage beer) (animal dogs))
Jess> (run)
I like dogs
I like beer
I like lobster
3
Note that the or CE does not represent an exclusive
or; if multiple branches of an or CE match existing
facts, the rule will fire multiple times.
When writing rules using the or CE you must assure that any
variables used on the right-and side of the rule will be bound on each
branch of the rules's left-hand-side.
In Jess 6.0a2, the or CE does not nest properly with the
not CE; this will be fixed in a future version. or
and and CEs can be nested arbitrarily.
2.8.1.5. The 'and' conditional element.
The and CE groups patterns together that must be
simulataneously matched. All rules have an implicit and CE
around their entire left-hand side, so by itself, the and CE
isn't very useful. It can be used together with other CEs to achieve
useful effects. For example, you can clean the floor if you have
either a vacuum cleaner or a broom and dustpan:
Jess> (defrule have-necessary-equipment
(or (item vacuum-cleaner)
(and (item broom)
(item dustpan)))
=>
(printout t "I can clean up this sawdust." crlf))
In Jess 6.0a2, the and CE does not nest properly with
the not CE; this will be fixed in a future version.
2.8.1.6. 'Not' patterns.
A pattern can be enclosed in a list with not as the head. In this
case, the pattern is considered to match if a fact which matches the pattern
is not found. For example:
Jess> (defrule example-7
(person ?x)
(not (married ?x))
=>
(printout t ?x " is not married!" crlf))
Note that a not pattern cannot define any variables that are
used in subsequent patterns (since a not pattern does not match
any facts, it cannot be used to define the values of any variables!) You
can introduce variables in a not pattern, so long as they are
used only within that pattern; i.e,
Jess> (defrule no-odd-numbers
(not (number ?n&:(oddp ?n)))
=>
(printout t "There are no odd numbers." crlf))
Similarly, a not pattern can't have a pattern binding.
A not CE is evaluated only when either a fact matching it
exists, or when the pattern immediately before the not on the
rule's LHS is evaluated. If a not CE is the first pattern on
a rule's LHS, the pattern (initial-fact) is inserted to
become this important preceding pattern. Therefore, the fact
(initial-fact) created by the
reset
command
is necessary to the proper functioning of some not
patterns. For this reason, it is especially important to issue a
reset command before attempting to run the rule engine when
working with not patterns.
not CEs can be nested to produce some interesting
effects (see the discussion of the
exists CE). In Jess 6.0a2, the not CE does
not nest properly with the and and or CEs; this will
be fixed in a future version.
2.8.1.7. The 'test' conditional element.
A pattern with test as the head is special; the body consists
not of a pattern to match against the knowledge base but of a single
boolean function which is evaluated and whose truth determines whether the
pattern matches. For example:
Jess> (deftemplate person (slot age))
Jess> (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))
A test CE is evaluated every time the preceding
pattern on the rule's LHS is evaluated. Therefore the following two
rules are precisely equivalent in behaviour:
Jess> (defrule rule_1
(foo ?X)
(test (> ?X 3))
=>)
Jess> (defrule rule_2
(foo ?X&:(> ?X 3))
=>)
For rules in which a test CE is the first pattern on the LHS,
the pattern (initial-fact) is inserted to become the
"preceding pattern" for the test. The fact
(initial-fact) is therefore also important for the proper
functioning of the test conditional element; the caution about
reset
in the preceding section applies
equally to test.
2.8.1.8. The 'unique' conditional element.
A pattern can be enclosed in a list with unique as the head.
This is a hint to Jess that only one fact could possibly satisfy a given
pattern, given matches for the preceding patterns in that rule. Here's
an example:
Jess> (deftemplate tax-form (slot social-security-number))
Jess> (deftemplate person (slot social-security-number) (slot name))
Jess> (defrule unique-demo
(tax-form (social-security-number ?num))
(unique (person (social-security-number ?num) (name ?name)))
=>
(printout t "Auditing " ?name "..." crlf))
Here the unique CE is providing a hint to Jess that only one person
can have a given Social Security number. Given this knowledge, Jess knows
that once it has found the person that matches a given tax form, it doesn't
need to look any further. In practice, this can result in performance gains
of 20-30% on real problems.
unique may not be combined in the same patten with either
test or not CEs.
Prolog users may recognize that unique is quite similar to
that language's ! (cut) operator.
2.8.1.9. The 'exists' conditional element.
A pattern can be enclosed in a list with exists as the head.
An exists CE is true if there exist any facts that match the pattern,
and false otherwise. exists is useful when you want a rule to fire
only once, although there may be many facts that could potentially activate
it.
Jess> (defrule exists-demo
(exists (honest ?))
=>
(printout t "There is at least one honest man!" crlf))
If there are any honest men in the world, the rule will fire once and
only once.
exists may not be combined in the same pattern with
a test CE.
Note that exists is precisely equivalent to (and in fact,
is implemented as) two nested not CEs; i.e., (exists
(A)) is the same as (not (not (A))).
2.8.1.10. Node index hash value.
The node index hash value is a tunable performance-related
parameter that can be set globally or on a per-rule basis. A small
value will save memory, possibly at the expense of performance; a
larger value will use more memory but lead to faster rule LHS execution.
In general, you might want to declare a large value for a rule that
was likely to generate many partial matches (prime numbers are the
best choices:)
Jess> (defrule nihv-demo
(declare (node-index-hash 169))
(item ?a)
(item ?b)
(item ?c)
(item ?d)
=>)
See the discussion of the
set-node-index-hash
function for a full discussion of this value and what it means.
2.8.1.11. Forward and backward chaining
The rules we've seen so far have been forward-chaining rules, which
basically means that the rules are treated as if... then
statements, with the engine passively executing the RHSs of activated
rules. Some rule-based systems, notable Prolog and its derivatives,
support backward chaining. In a backwards chaining system,
rules are still if... then statements, but the engine seeks
steps to activate rules whose preconditions are not met. This
behaviour is often called "goal seeking". Jess supports both forward
and backward chaining. Note that the explanation of backward chaining
in Jess is necessarily simplified here since full explanation requires
a good understanding of the underlying
algorithms used by Jess.
To use backward chaining in Jess, you must first declare that certain
fact templates will be backward chaining reactive using the
do-backward-chaining
function:
Jess> (do-backward-chaining factorial)
Then you can define rules which match such patterns.
Jess> (defrule print-factorial-10
(factorial 10 ?r1)
=>
(printout t "The factorial of 10 is " ?r1 crlf))
When the rule compiler sees that a pattern matches a
backward chaining reactive template, it rewrites the rule and inserts
some special code into the internal representation of the rule's
LHS. This code asserts a fact onto the fact-list that looks like
(need-factorial 10 nil)
if, when the rule engine is reset, there are no matches for this
pattern. The head of the fact is constructed by taking the head of the
reactive pattern and adding the prefix "need-".
Now, you can write rules which match these need-(x) facts.
Jess> (defrule do-factorial
(need-factorial ?x ?)
=>
(bind ?r 1)
(bind ?n ?x)
(while (> ?n 1)
(bind ?r (* ?r ?n))
(bind ?n (- ?n 1)))
(assert (factorial ?x ?r)))
The rule compiler rewrites rules like this too: it adds a
negated match for the factorial pattern itself to the rule's LHS.
The end result is that you can write rules which match on (factorial),
and if they are close to firing except they need a (factorial) fact to
do so, any (need-factorial) rules may be activated. If these rules
fire, then the needed facts appear, and the (factorial)-matching rules
fire. This, then, is backwards chaining! Jess will chain backwards
through any number of reactive patterns. For example:
Jess> (do-backward-chaining foo)
TRUE
Jess> (do-backward-chaining bar)
TRUE
Jess> (defrule rule-1
(foo ?A ?B)
=>
(printout t foo crlf))
TRUE
Jess> (defrule create-foo
(need-foo $?)
(bar ?X ?Y)
=>
(assert (foo A B)))
TRUE
Jess> (defrule create-bar
(need-bar $?)
=>
(assert (bar C D)))
TRUE
Jess> (reset)
TRUE
Jess> (run)
foo
3
In this example, none of the rules can be activated at first. Jess
sees that rule-1 could be activated if there were an
appropriate foo fact, so it generates the request (need-foo
nil nil). This matches part of the LHS of rule
create-foo cannot fire for want of a bar fact. Jess
therefore creates a (need-bar nil nil) request. This matches
the LHS of the rule create-bar,which fires and asserts
(bar C D). This activates create-foo, which fires,
asserts (foo A B), thereby activating rule-1, which
then fires.
There is a special conditional element, (explicit), which you
can wrap around a pattern to inhibit backwards chaining on an otherwise
reactive pattern.
2.8.2. Defqueries
The defquery construct lets you create a special kind of rule
with no right-hand-side. While defrules act spontaneously,
defqueries are used to search the knowledge base under direct program
control. A rule is activated once for each matching set of facts,
while a query gives you a java.util.Enumeration of all the
matches. An example should make this clear. Suppose we have defined
this defquery:
Jess> (defquery search
"Finds foo facts with a specified first field"
(declare (variables ?X))
(foo ?X ?Y))
Then if the knowledge base contains these facts:
Jess> (deffacts data
(foo blue red)
(bar blue green)
(foo blue pink)
(foo red blue)
(foo blue blue)
(foo orange yellow)
(bar blue purple))
Then the following Jess code Will print the output shown:
Jess> (reset)
Jess> (bind ?e (run-query search blue))
Jess> (while (?e hasMoreElements)
(printout t (nth$ 2 (call (call (call ?e nextElement) fact 1) get 0)) crlf))
red
pink
blue
FALSE
because these three values follow blue in a foo
fact. Note that each match begins with an extra fact - a
__query-trigger fact that triggers the matching process,
asserted by the run-query command; hence the argument to the call to
Token.fact() above is 1, not 0.
The following Java code is similar to the Jess snippets above.
It defines the same query and deffacts, runs the query and then
collects the red, pink and blue values in a Vector
as Strings.
import jess.*;
import java.util.*;
public class ExQuery
{
public static void main(String [] argv) throws JessException
{
// Create engine, define query and data
Rete r = new Rete();
r.executeCommand("(defquery search (declare (variables ?X)) (foo ?X ?Y))");
r.executeCommand("(deffacts data" +
"(foo blue red)" +
"(bar blue green)" +
"(foo blue pink)" +
"(foo red blue)" +
"(foo blue blue)" +
"(foo orange yellow)" +
"(bar blue purple))");
// Assert all the facts
r.reset();
// Run the query, store the result
r.executeCommand("(store RESULT (run-query search blue))");
// Fetch the result (an Enumeration).
Enumeration e = (Enumeration) r.fetch("RESULT").externalAddressValue(null);
Vector v = new Vector();
// Pick each element of the Enumeration apart and store the
// interesting part in the Vector v.
while (e.hasMoreElements())
{
Token t = (Token) e.nextElement();
// We want the second fact in the token - the first is the query trigger
Fact f = t.fact(1);
// The first and only slot of this fact is the __data multislot.
ValueVector multislot = f.get(0).listValue(null);
// The second element of this slot is the datum we're interested in.
v.addElement(multislot.get(1).stringValue(null));
}
for (Enumeration answers = v.elements(); answers.hasMoreElements();)
System.out.println(answers.nextElement());
}
}
C:\> java
red
pink
blue
Defqueries can use virtually all of the same features that rule LHSs
can, except for salience. The function
ppdefrule
can also
pretty-print defqueries.
2.8.2.1. The variable declaration
You might have already realized that two different kinds of variables
can appear in a query: those that are "internal" to the query, like ?Y
in the query above, and those that are "external", or to be specified in the
run-query
command when the query is executed. Jess assumes
all variables in a query are internal by default; you must declare any
external variables explicitly using the syntax
(declare (variables ?X ?Y ...))
which is quite similar to the syntax of a rule salience declaration.
2.8.2.2. The run-query command
The
run-query
command
lets you supply values for the external variables of a query and
obtain a list of matches. This function returns a
java.util.Enumeration of
jess.Token object, one for
each matching combination of facts. The example code above calls
fact(0) on each jess.Token, to get the first jess.Fact
object from the jess.Token, then calls get(0) on the
fact to get the data from the first slot (which for ordered facts, is
a multislot named __data; see the
documentation for jess.Fact) and then uses (nth$ 2)to get the
second entry in that multislot.
Note that each token will contain one more fact than there are
patterns on the query's LHS; this extra fact is used internally by
Jess to execute the query.
You must supply exactly one value for each external variable of the
named query.
2.8.2.3. The count-query-results command
To obtain just the number of matches for a query, you can use the
count-query-results
function. This function accepts the same arguments as
run-query, but returns an integer, the
number of matches.
2.8.2.4. The future of queries
defquery is a new feature, and the syntax may change; in
particular, a simpler mechanism for obtaining query results may be
defined. Suggestions are welcome.
Back to index