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!
Jess>
Another useful function is batch. batch evaluates a
file of Jess code. To run the Jess source file
examples/wordgame.clp you can enter
Jess> (batch examples/wordgame.clp)
The problem is
GERALD
+ DONALD
...
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:
(bind $?grocery-list (create$ 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> ?x
"The value"
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 at 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:
(deffunction max (?a ?b)
(if (> ?a ?b) then
(return ?a)
else
(return ?b)))
Note that this could have also been written as:
(deffunction max (?a ?b)
(if (> ?a ?b) then
?a
else
?b))
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.
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.
Jess> (defadvice after + (return (- ?retval 1)))
TRUE
Jess> (+ 2 2)
3
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> (call (get-member java.lang.System in) read)
A
65
Note that 65 is the ASCII code for 'A'.
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, long, or their wrappers |
RU.INTEGER |
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.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 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 mroe 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:
(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.
TRUE
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> (assert (box (location kitchen) (contents spatula sponge frying-pan)))
<Fact-1>
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 1 (location dining-room))
TRUE
Jess> (facts)
f-0 (initial-fact)
f-2 (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:
(deftemplate used-auto extends automobile
(slot mileage)
(slot blue-book-value)
(multislot owners))
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)
(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 (used-auto (make Saturn) (model SL1) (year 1992) (color white)
(mileage 120000) (blue-book-value 3500) (owners ejfried))
For a total of 3 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 knowledge base.
An example will probably help at this point. Let's say you have the
following Java Bean class
public class SimpleBean
{
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 SimpleBean)
SimpleBean
Jess> (show-deftemplate simple)
(deftemplate simple "$JAVA-OBJECT$ SimpleBean"
(slot class (default <External-Address:java.beans.PropertyDescriptor>))
(slot name (default <External-Address:java.beans.PropertyDescriptor>))
(slot OBJECT (type 2048)))
TRUE
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 SimpleBean 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 objecte to the
knowledge base.
Jess> (bind ?sb (new SimpleBean))
<External-Address:SimpleBean>
Jess> (definstance simple ?sb static)
TRUE
Jess> (facts)
f-0 (simple (class <External-Address:java.lang.Class>)
(name "Bob") (OBJECT <External-Address:SimpleBean>))
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:SimpleBean>))
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 (simple (class <External-Address:java.lang.Class>)
(name "Fred") (OBJECT <External-Address:SimpleBean>))
For a total of 1 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
Jess50b1/jess/examples/simple directory.
defclasses, 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.
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:
(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:
(defrule wrong-rule
(eq (+ 2 2) 4)
=>
(printout t "Just as I thought, 2 + 2 = 4!" crlf))
This rule will NOT fire just because the function call (eq (+ 2 2) 4)
would evaluate to true. Instead, Jess will try to find a fact on the
knowledge base that looks like (eq 4 4). 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+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 thereset 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 file
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:
(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:
(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))
TRUE
Jess> (run)
I need to buy (eggs milk bacon)
TRUE
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:
(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.
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:
(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. '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:
(defrule example-7
(person ?x)
(not (married ?x))
=>
(printout t ?x " is not married!" crlf))
Note that a not pattern cannot contain any variables that are
not bound before that pattern (since a not pattern does not match
any facts, it cannot be used to define the values of any variables!) You
can use blank variables, however (a blank variable is a bare ?
or $?). A not pattern can similarly not have a pattern
binding.
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.
2.8.1.5. The 'test' conditional element (CE).
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:
(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:
(defrule rule_1
(foo ?X)
(test (> ?X 3))
=>)
(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.6. 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:
(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.7. 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:)
(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.8. 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:
(do-backward-chaining factorial)
Then you can define rules which match such patterns.
(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.
(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
(defrule rule-1
(foo ?A ?B)
=>
(printout t foo crlf))
TRUE
(defrule create-foo
(need-foo $?)
(bar ?X ?Y)
=>
(assert (foo A B)))
TRUE
(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
cerate-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:
(defquery search
"Finds foo facts with a specified first field"
(declare (variables ?X))
(foo ?X ?Y))
Then if the knowledge base contains these facts:
(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
(bind ?e (run-query search blue))
(while (?e hasMoreElements)
(printout t (nth$ 2 (call (call (call ?e nextElement) fact 0) get 0) crlf))
Will print
red
pink
blue
because these three values follow blue in a foo
fact.
Defqueries can use virtually all of the same features that rule LHSs
can, except for salience.
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 before
the final 5.0 release; in particular, a simpler mechanism for
obtaining query results may be defined. Suggestions are welcome.