Jess stands for Java Expert System Shell. It was developed by Ernest Friedmal-Hill at Sandia National Lab. It's a rule engine and scripting environment similar to CLIPS developed by NASA. Jess is totally written in Java platform of Sun Microsystems. Jess is a forward chaining production system that uses RETE algorithm. It's free for academic purpose and can be downloaded from http://herzberg.ca.sandia.gov/jess/. There is no need for your to install Jess in your machine as it is installed already in CS Linux machines. You could use it by simply connecting to CS machines through ssh. This tutorial covers most of the things that you need to know to implement the project. But, not all the functionalities/functions in Jess are covered in this tutorial. You are recommended to read the user's manual in the above webpage also.
Sample programs in jess can be found at the following directory:To run JESS, just type
> jess
Typing the above command, will put you in "Jess>" prompt. This is
the shell that you are going to interact with. To exit from this shell, you
should type and for any command, remember those brackets!
Jess> (exit)
You can use your UNIX system commands even from the Jess prompt by the
following way.
Jess>(system
pwd)
/users/yzhang/jess
Jess>
Though you can write Java programs and use the rete engine of Jess, it is
sufficient that you interact with Jess shell alone. You don't need to know Java
to program in Jess. You basically need to know only the Jess scripting language.
You can define facts and rules from the command prompt. Jess stores them in its
internal knowledge base, but never gives you access after you clear the memory.
So, type your script in a separate file and call that file from Jess prompt. You
can use whatever editor you want to edit your file. If the file name is cat.clp,
you can load it by
Jess> (batch
cat.clp)
Jess>
The comments/documentation can be written by
preceding it with a ";" character.
;; Agent moves
forward
All the facts that you give gets an id and goes into the knowledge base. The
rules go to the rule base. When you run a Jess program, it runs as long as it
doesn't find any rule to fire or gets a termination signal which is (halt)
equivalent to exit()/System.exit() in other languages. In a sense, execution of
a Jess program is basically firing of rules that keeps modifying the KB until
the goal is reached. After you load your program using batch command, you
should use the command reset to clear the working memory and load the
facts into it. To run your program, use the command run. You can use
clear to clear the working memory, but you should consult (batch)
your file again after using this command.
Jess>
(batch cat.clp) ;; loads the facts and rules into knowlege
base
Jess> (reset) ;; clear the working memory loads them into
working memory
Jess> (run) ;; runs the script
Jess>
(clear) ;; clears Jess (after this reset or run will not work)
The atoms is the basic unit in Jess representation. It can contain letters or numbers or characters like $,*,=,+,/,<,>,_,?,#. The most important thing to note is that everything in Jess is CASE SENSITIVE. This might be the major source of errors when you start programming in Jess. The boolean values in Jess are TRUE and FALSE (again, they are case sensitive). The Jess equivalent of C's NULL or Java's null is nil.
Jess allows floating point and integer numbers. They are like any other language that you would have programmed with.
The strings should be enclosed between double quotes. You can use the escape
character if you want Jess to ignore double quotes. str-length can be
used to find the length of a string. str-cmp will let you compare
two strings. It returns 0 if the two strings are identical, else a
negative/positive value.
Jess> (str-length
"howdy")
5
Jess> (str-length "how\"dy")
6
Jess> (str-compare
"Aggie" "Aggie")
0
List is a collection of one or more atoms, strings, numbers or lists between
parentheses. As you can see, list is the base for the whole syntax of
Jess.
Jess> (+ 1 2)
3
The variables should be denoted with a preceding "?". You can use bind
to associate something to a variable.
Jess>
(bind ?x "Howdy")
You don't have to declare variables before using them.
Jess> (bind ?x 1)
1
Jess> (= ?x
1)
TRUE
Jess> (> ?x 2)
FALSE
Multi-field variables are special variables with $ sign preceding them
and they refer to a special kind of list. The following functions can be used
with these variables.
Jess> (bind $?courses (create$ 625 629 613
606)) ;; creates a multi-field
list and assign to multi-field variable
(625 629 613
606)
Jess> $?courses
(625 629 613 606)
Jess> (first$ $?courses)
;; returns the first element
(625)
Jess> (rest$
$?courses)
;; returns rest of the elements
(629 613 606)
Jess>
(complement$ $?courses (create$ 601 602 603 604 605 629 613)) ;;
returns all elements of second multi-field not in first one
(601 602 603
604 605)
Jess> (delete$ (create$ 100 101 102 103 104) 2 4)
;; deletes between the range 2 and 4 (between
2nd and 4th element in list)
(100 104)
All local variables that you might create will be destroyed when you execute
the command reset. You can also have global variables. The global
variables will persist even after using (reset) (i.e. even if you assign
the variable to something else, it will still remain the initial one after
reset). If you want the global variable to reflect the change even after
reset, use (set-reset-globals nil)
Jess>
(defglobal ?*a* = 100)
TRUE
Jess> ?*a*
100
Jess> (bind
?*a* 50)
50
Jess> (reset)
TRUE
Jess>
?*a*
100
The following functions help in type checking (to see whether the variable is an integer etc and to cast a variable to another type. The examples below would be self explanatory.
- (eq <expression> <expression>+) ;;
returns true if 1st exp is of the same type as others
- (neq
<expression> <expression>+) ;; returns true if 1st exp is of
the same type and value as others
- (integerp
<expression>)
;; returns true if the exp is an integer
- (numberp
<expression>)
;; returns true if the exp is a number
Jess>
(bind ?x 1.4)
1.4
Jess> (eq ?x 1)
FALSE
Jess> (integerp
?x)
FALSE
It's similar to Java's typecasting. Generally, the use of casting in Jess won't be that common in your code.
- (float <numeric-expression>) ;;
casting to float
- (integer <numeric-expression>) ;;
casting to integer
Jess> (bind ?x 1.4)
1.4
Jess> (= ?x
1)
FALSE
Jess> (= (integer ?x) 1)
TRUE
The arithmetic operators are the same in Jess as any other standard
programming language. But, you have to represent them in prefix form. If you
want to say x++ somewhere in your program, you should say
(bind ?x (+ ?x 1))
For testing equality/inequality, you can use the operators in the same way.
Remember, = is a comparison operator. For assignment, you should use
bind.
Jess> (bind ?x
1)
1
Jess> (= ?x 1)
TRUE
Jess> (> ?x
2)
FALSE
There are three logical operators available in Jess. They are and,
or and not. The following example will help you understand their
usages and functionalities.
Jess> (bind ?x
1)
1
Jess> (and (= ?x 1) (< ?x 0))
FALSE
Jess> (or (= ?x 1)
(< ?x 0))
TRUE
Jess> (not (= ?x 5))
TRUE
As any
other programming language, Jess allows the use of if-then-else statements and
while loops.
- (if <expression> then <actio>+
else <action>+)
- (while <expression> do <action>+)
The facts are stored in the knowledge base. You can do following things with facts.
You can use assert to add a fact to the KB. Assert is especially helpful when you want to add a fact to a KB when a rule is fired or during the execution of the program. The examples are from the example code cat.clp.
(assert (Room (x 5) (has-milk TRUE))) ;; asserts that 5th room has milk
However, to definite the initial condition of the world, it would be easy to use deffacts to assert all facts in one shot.
(deffacts init-room
(Room (x 1))
(Room (x 2))
(Room (x 3))
(Room (x 4))
(Room (x 5)
(has-milk TRUE))
)
You can remove facts using retract. However, the usage of retract
isn't that simple as you have to first get the fact-id of the fact the concerned
fact that you want to remove from KB and then remove it.
....
?room <- (Room (x 5) (has-milk TRUE)) ;; gets the fact id
=>
(retract
?room)
;; removes the fact
You can roughly compare templates to a struct in C or Class in C++/Java. This
is a very handy tool in representing different worlds. The fields inside a
template are called slots. You should use deftemplate to define a
template. The following example would be self explanatory on it's
usage.
(deftemplate Cat
(slot x (type
integer) (default 1))
(slot status (default hungry) )
(slot
smells-milk (default FALSE))
)
You can use modify to change the values of a slot. Look at the
following example.
...
?cat <- (Cat(x
5))
=>
(modify ?cat (smells-milk TRUE))
Rules are responsible for taking actions based on the facts in the KB. The rules have a left-hand-side part and a right-hand-side part with the "implies" operator in between. In a sense, all rules can be compared to a if-else statement in procedural languages. In procedural languages, the statements are executed line by line and only once. But, Jess fires all the rules continuously as long as it's left-hand-side is satisfied. You should use defrule to define a rule. The following example defines the rule for moving the cat from current room to the next room.
(defrule move
?cat <- (Cat(x ?x)
(smells-milk FALSE))
=>
(modify ?cat (x (+ ?x 1) ))
(printout t "Cat moves to ["?x"]." crlf)
)
The above
rule would be fired whenever the cat doesn't smell milk. Assume, we defined the
initial states using deffunction and I want to ask the user whether he is
ok with the default values of the location of milk. Now, we might have a
conflict of which of these two rules should be executed? In procedural
languages, it is easy as the code is executed line by line in a timely fashion.
Here, if we have two or more rules with all preconditions satisfied, you should
assign some priority (refered as salience) to each rule. The salience should be
assigned while you define the rule and it is just a simple integer value. So, in
a conflicting scenario, a rule with higher salience would be always executed. We
can add the salience value as follows.
(defrule move
(declare (salience
50))
...
=>
....
)
In the above discussions, we went through several inbuilt functions in Jess. In addition, you can also define your own function so that you don't have to dump everything inside rules. You should use deffunction to define a function. You can call the following function by calling (change-default)
(deffunction change-default ()
......
(assert (Room (x ?x) (has-milk TRUE)))
)
There are two functions available for taking input from Standard input (your
terminal). You can use read to read a single atom or string or number.
You can also use readline to read an entire line as a string.
-
(read <router-identifier>) ;; router-identifier has to be t
for standard input - specifying t is optional
- (readline
<router-identifier>) ;; --same--
Jess>
(read t)
or inside the code
(if (eq
(read) y) then
(retract ?room)
(change-default)
)
To write to the terminal, you can use printout. As we saw for read, you would specify the router-identifier which is again t as you are printing to the standard output (your terminal).
- (printout <router-identifier> <expression>)
Jess> (printout t "Howdy!"
crlf)
Howdy!
File I/O is easy in Jess. Use the open function with a
<router-identifier> to open a file with read/write/append mode. Then, to
read the contents of the file, you can use the read/readline function
using the same router-identifier that you used for opening the file. Similarly,
to write to a file, you can use printout function using the same
router-identifier that you used for opening the file.
- (open
<file-name> <router-identifier> [r or w or a])
The following
example would clearly illustrate the usages of the above functions.
Jess> (open temp.txt id w)
;;
open a file temp.txt in write mode
id
;;
returns the router-identifier
Jess> (printout id "Howdy!!"
crlf) ;; writes the string "Howdy!" into the file
corresponding to the router-identifier id
Jess> (open temp.txt id
r)
;; opens the file temp.txt in read mode
id
Jess> (read
id)
;;
reads the stuff from the file opened using the
router-identifier
Howdy!!
Jess> (system cat
temp.txt)
;; just
double-checking - reading the contents using unix command
cat
Howdy!!
<External-Address:java.lang.UNIXProcess>
To take the output of your program, you can use the unix command "script <output-file>". After executing this command, do all your stuff with Jess (run your program) and then come back to % prompt and say "exit". This will write all your transactions into the output file. Please follow the following sequence and avoid using screenshots
> script output.txt
%java jess.Main
Jess>
(run)
..
...
Jess> (exit)
%exit
>more
output.txt
Debugging is a challenging task in any language. In procedural languages, printing the variables would help a lot. In Jess, you can use printout function to print stuffs to the terminal. But, it won't be sufficient. You will be interested in seeing which rule fires, what are the current facts in the KB? etc.
The function (watch all) would tell you the different states as the program
executes. (equivalent to running in debug mode). Also you can use (facts)
command that would list the functions in the knowledge base. (you should use
reset before using facts). Also you can use (rules) command
that would list the rules from the rule base. You can try the following
sequence.
Jess> (watch all)
Jess> (batch
cat.clp)
Jess> (reset)
Jess> (facts)
Jess>
(rules)
Pretty print is another nice utility that would print
the concerned rule/function/template. The respective functions are ppdefrule,
ppdeffunction and ppdeftemplate. The following example shows the
usage of ppdeftemplate.
Jess> (ppdeftemplate Cat)
(deftemplate Cat
extends __fact ""
(slot x (default 1) (type 4))
(slot status (default
hungry))
(slot smells-milk (default FALSE)))
Since, Jess
has been developed in Java, it would throw an exception when something goes
wrong. Read the exception message carefully as it will give an idea of the
problem and the line number corresponding to that.