5. Working Memory
Each Jess rule engine holds a collection of knowledge nuggets called facts. This collection is known as the working memory. Working memory is important because rules can only react to additions, deletions, and changes to working memory. You can't write a Jess rule that will react to anything else.
Some facts are pure facts defined and created entirely by Jess. Other facts are shadow facts connected to Java objects you provide. Shadow facts act as "bridges" that let Jess reason about things that happen outside of working memory.
Every fact has a template. The template has a name and a set of slots, and each fact gets these things from its template. This is the same structure that JavaBeans -- plain old Java objects, or POJOs -- have, and it's also similar to how relational databases are set up. The template is like the class of a Java object, or like a relational database table. The slots are like the properties of the JavaBean, or the columns of a table. A fact is therefore like a single JavaBean, or like a row in a database table. You can think of it either way.
In Jess, there are three kinds of facts: unordered facts, shadow facts and ordered facts. We'll learn about all of these in this chapter. First, though, we need to learn more about templates.
Just so you know, as we learn about working memory: you can see a list of all the facts in working memory using the facts command. Facts are added using the assert, add, and definstance functions. You can remove facts with the retract and undefinstance functions. The modify function lets you change the slot values of facts already in working memory. And finally, you can completely clear Jess of all facts and other data using the clear command.
5.1. Templates
As we've already stated, every fact has a template. A fact gets its name and its list of slots from its template. Therefore a template is something like a Java class.
You usually create templates yourself using the deftemplate construct or the defclass function. Other times, templates are created automatically for you, either while you're defining a defrule or when you use the add function or the jess.Rete.add(java.lang.Object) method.
The deftemplate construct is the most general and most powerful way to create a template. You won't understand most of the options shown here yet, but we'll cover them all in this chapter and the next:
(deftemplate template-name [extends template-name] ["Documentation comment"] [(declare (slot-specific TRUE | FALSE) (backchain-reactive TRUE | FALSE) (from-class class name) (include-variables TRUE | FALSE) (ordered TRUE | FALSE))] (slot | multislot slot-name ([(type ANY | INTEGER | FLOAT | NUMBER | SYMBOL | STRING | LEXEME | OBJECT | LONG)] [(default default value)] [(default-dynamic expression)] [(allowed-values expression+)])*)
A template declaration includes a name, an optional documentation string, an optional "extends" clause, an optional list of declarations, and a list of zero or more slot descriptions. Each slot description can optionally include a type qualifier or a default value qualifier. In the syntax diagram, defaults for various options are indicated in bold letters.
The template-name is the head of the facts that will be created using this template.
The declarations affect either how the template will be created, or how facts that use it will act, or both. We'll cover most of the declarations in this chapter; others are covered in the Constructs appendix or in the chapter on rules.
Every template has a single parent template, which can be specified with the "extends" clause. A template inherits all the slots of its parent template, as well as declared properties like slot-specific and backchain-reactive. If you don't specify a parent, a template extends a template named "__fact", which has no slots.
Some template declarations include slots (those that don't will generally have implicitly defined slots.) There may be an arbitrary number of slots in a template. Each <slot-name> must be a symbol. A multislot is a slot that can hold a list of values, while a normal slot can hold just one value at a time. The name of a slot may not contain a '.' (dot) character.
Each slot can have a list of zero or more slot qualifiers. The default slot qualifier gives a value to use for a slot when the fact is first created, if no other value is specified; the default is the symbol nil for a regular slot, and the empty list for a multislot. default-dynamic is similar but the the given expression will be evaluated each time a new fact using this template is asserted.
The type slot qualifier is accepted but not currently enforced by Jess; in theory it specifies what data type the slot is allowed to hold. Acceptable values are ANY, INTEGER, FLOAT, NUMBER, SYMBOL, STRING, LEXEME, and OBJECT.
The allowed-values slot qualifier gives a set of values allowed to be in the slot. For a multislot, the allowed values are restricted to values in this set. If you specify both allowed-values and default, Jess checks to make sure they're consistent. If you specify allowed-values but do not specify a slot default, then the first listed allowed value becomes the default.
5.1.1. Undefining templates
Although it shouldn't be a common requirement, you can remove a previously defined template using the jess.Rete.removeDeftemplate(String) method. You might want to do this during interactive development, because you can't change the slots of a template without removing the old definition first. You won't be able to remove a template unless it's completely unused: no rules, other templates, deffacts, or facts may reference it.
5.2. Unordered facts
In object-oriented languages like Java, objects have named fields in which data appears. Unordered facts offer this capability (although the fields are traditionally called slots.)
(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.
As an example, defining the following template:
Jess> (deftemplate automobile "A specific car." (slot make) (slot model) (slot year (type INTEGER)) (slot color (default white)))
would allow you to define the fact shown here.
Jess> (reset)
Jess> (assert (automobile (model LeBaron) (make Chrysler) (year 1997)))
<Fact-1>
Jess> (facts)
f-0 (MAIN::initial-fact) f-1 (MAIN::automobile (make Chrysler) (model LeBaron) (year 1997) (color white)) For a total of 2 facts in module MAIN.
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 template.
Note also 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.
As you can see above, each fact is assigned an integer index (the fact-id) when it is asserted. You can remove an individual fact from the working memory using the retract function.
Jess> (retract 1)
TRUE
Jess> (facts)
f-0 (MAIN::initial-fact) For a total of 1 facts in module MAIN.
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.
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-2>
(We're saving the fact 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-2>
Jess> (facts)
f-0 (MAIN::initial-fact) f-2 (MAIN::box (location dining-room) (contents spatula sponge frying-pan)) For a total of 2 facts in module MAIN.
The optional extends clause of the deftemplate construct lets you define one template 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, in the same order, 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.
5.3. Shadow facts: reasoning about Java objects
As mentioned previously, shadow facts are just unordered facts that serve as "bridges" to Java objects. By using shadow facts, you can put any Java object into Jess's working memory.
5.3.1. Templates for shadow facts
Like all other facts, shadow facts need to have a template, In this case, though, rather than specifying the slots ourselves, we want to let Jess create the template automatically by looking at a Java class. For example, we might be writing a banking program. Our imaginary Java code works with Account objects, like this:
import java.io.Serializable; public class Account implements Serializable { private float balance; public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } // Other, more interesting methods }
At some point the rule-based part of our program needs to deal with these too. Therefore we'll need a template like this:
Jess> (deftemplate Account (declare (from-class Account)))
Note how I've used the class name (minus the package prefix, if there was one) as the template name. This is just a convention, and the template name can be anything you want. But as we'll see a little later, Jess knows about this convention and using it can save a little work.
This template will automatically have slots corresponding to the the JavaBeans properties of the Account class: in particular, there will be a slot named balance corresponding to the getBalance() method. Jess uses the java.beans.Introspector class to find the property names, so you can customize how Jess defines properties by writing your own java.beans.BeanInfo classes.
The defclass function can be used instead to create a shadow fact template; it looks like
Jess> (defclass Account Account)
It is just a shortcut for using deftemplate. The chief advantage of defclass is that because it is a function, it can be used anywhere, unlike deftemplate which is a construct, and can only be used at the top level of a program (see here for more information.) The chief disadvantage is that it is limited in what it can do: many of the declarables that are available with deftemplate are not available with defclass.
The from-class declaration, by itself, will create slots that correspond to JavaBeans properties. If you also use include-variables, like this:
Jess> (deftemplate Account (declare (from-class Account) (include-variables TRUE))
then public member variables of the class are used to define additional slots.
5.3.2. Adding Java objects to working memory
Once you've created an appropriate template, you can add some Java objects to working memory, automatically creating shadow facts to link them to Jess. Continuing with our Account example, imagine that you want to create an Account, and you want to add this object to working memory because rules will be working with it. Then all you need to do is:
Jess> (bind ?a (new Account))
<Java-Object:Account>
Jess> (add ?a)
<Fact-0>
Jess> (facts)
f-0 (MAIN::Account (balance 0.0) (class <Java-Object:java.lang.Class>) (OBJECT <Java-Object:Account>)) For a total of 1 facts in module MAIN.
The add function creates a shadow fact and links it to our Account object. We'll explore the nature of that link in the following section, but before we do that, I want to point out two things about add and about shadow fact templates in general.
First, note that the template has a slot named OBJECT. All shadow fact templates have this slot. Each shadow fact created from this template will use that slot to hold a reference to the Java object itself. Therefore, the original object is always easily available. The reverse mapping (given a Java object, finding its shadow fact) is also available using the method jess.Rete.getShadowFactForObject(java.lang.Object).
Second, the add function understands the template naming convention discussed in the previous section. If you call add without creating a matching template first, add will create a template automatically, using that naming convention.
5.3.3. Responding to changes
Continuing with our Account example, we can use the modify function to change the fact, and the Java object will be automatically modified too:
Jess> (printout t (?a getBalance) crlf)
0.0
Jess> (modify 0 (balance 1))
<Fact-0>
Jess> (facts)
f-0 (MAIN::Account (balance 1) (class <Java-Object:java.lang.Class>) (OBJECT <Java-Object:Account>)) For a total of 1 facts in module MAIN.
Jess> (printout t (?a getBalance) crlf)
1.0
But what happens if we modify the Account directly?
Jess> (printout t (?a getBalance) crlf)
1.0
Jess> (?a setBalance 2)
Jess> (printout t (?a getBalance) crlf)
2.0
Jess> (facts)
f-0 (MAIN::Account (balance 1) (class <Java-Object:java.lang.Class>) (OBJECT <Java-Object:Account>)) For a total of 1 facts in module MAIN.
The working memory still thinks our Account's balance is 1.0. There are several ways to notify Jess that the object is changed and working memory needs updating. One is using the update function:
Jess> (update ?a)
Jess> (facts)
f-0 (MAIN::Account (balance 2.0) (class <Java-Object:java.lang.Class>) (OBJECT <Java-Object:Account>)) For a total of 1 facts in module MAIN.
update tells Jess to refresh the slot values of the shadow fact linked to the given Java object. The reset function, which resets working memory, updates the slots of all shadow facts in the process. You can also actively tell Jess to refresh its knowledge of the properties of individual objects using the jess.Rete.updateObject(java.lang.Object) method.
This behaviour is what you get for objects that don't support property change notification. In practice, this is fine. Most rule-based programs reason about static "value" objects whose properties don't change over time, or change only occasionally or at well-defined times.
But if your objects will be changed often from outside of Jess, then it would be nice to have updates happen automatically. If you want to have your shadow 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 fulfill this requirement Jess will automatically arrange for working memory to be updated every time a property of the Bean changes. We can modify our Account class to support this feature like this:
import java.io.Serializable; import java.beans.*; import java.io.Serializable; public class PCSAccount implements Serializable { private PropertyChangeSupport pcs = new PropertyChangeSupport(this); private float balance; public float getBalance() { return balance; } public void setBalance(float balance) { float temp = this.balance; this.balance = balance; pcs.firePropertyChange("balance", new Float(temp), new Float(balance)); } public void addPropertyChangeListener(PropertyChangeListener pcl) { pcs.addPropertyChangeListener(pcl); } public void removePropertyChangeListener(PropertyChangeListener pcl) { pcs.removePropertyChangeListener(pcl); } // Other, more interesting methods }
Now whenever an Account balance is modified, Jess will be notified and the corresponding shadow fact will be updated immediately.
5.3.4. More about PropertyChangeEvents
PropertyChangeEvents and shadow facts seem magical to some people; the flow of control can be confusing, and the notion of things happening "automatically" sometimes makes people forget that no code executes in Java unless something, somewhere, invokes it. They're not magical, and to prove it, I'll explain exactly where they come from, what they do, and what Jess does when it sees one -- or not.
First, have a look back at the PCSAccount class above. It's a classic example of a JavaBean that supports PropertyChangeListeners. When you add an instance of this class to working memory, Jess will usually call addPropertyChangeListener(listener) on it, where listener is a Jess object designed to respond to change events. The listener object is added to a list of objects that want to be notified when the PCSAccount object's properties change. This list is managed by the PropertyChangeSupport object that the PCSAccount holds as a member (Why "usually?" If you use definstance to add the object to working memory and specify static as the final argument, then Jess will skip this step.)
Of course, many (most) Java classes don't allow you to add PropertyChangeListeners to them, and so for many of the Java objects you'll add to working memory, Jess doesn't register itself with the object in any way. The object is filed away in working memory, and that's it.
There are now two main scenarios of interest: an object in working memory can be modified from Jess code, or from Java code. Each of these scenarios can happen with or without PropertyChangeListeners, for a total of four cases. We'll discuss each of these four situations separately. The most important things to remember is that all of the steps discussed here happen synchronously: neither Jess, nor the PropertyChangeSupport class, will spawn any new threads to process change events. Everything happens on the thread that initiates the change. Now, on to the four cases:
- Object modified from Java, no PropertyChangeListeners. In this case, the Java code modifies the object, and Jess doesn't know about it. Bad things may happen as a result: Jess's working memory indices may need to be updated. Therefore you should always call the jess.Rete.updateObject(java.lang.Object) method to tell Jess when an object is changed in this way.
- Object modified from Java, PropertyChangeListeners. In this case, Java code calls a method like setBalance() above, which calls firePropertyChange(), which iterates over that list of listeners we mentioned previously and calls propertyChanged() on each listener. One of those listeners will be the Jess object. Jess will examine the PropertyChangeEvent object that propertyChange() receives as an argument and update working memory appropriately.
- Object modified from Jess, no PropertyChangeListeners. If you use modify (or the Java equivalent) to modify a shadow fact, then Jess will call the appropriate "setter" methods on the object, and update working memory.
- Object modified from Jess, PropertyChangeListeners. This is the most complicated case. Note that there is a problem: if Jess calls a setter method on the object, then the object will send a redundant PropertyChangeEvent back to Jess. Unless Jess is careful, there could be an infinite loop. What Jess does is to call the setter methods, and then rely on the object to send a PropertyChangeEvent back. When Jess receives the change event, it will update working memory in response. This works fine as long as property change notification is implemented correctly by the object. If the object accepts PropertyChangeListeners but then doesn't send PropertyChangeEvents, however, then working memory will not be updated. If you have a class that behaves this way and you can't fix it, then you can always use definstance with the static option so that Jess ignores the change support.
5.3.5. Shadow fact miscellany
Shadow fact templates, like all templates, can extend one another. In fact, shadow fact templates can extend ordinary templates, and ordinary templates can extend shadow fact templates. Of course, for a shadow fact template to extend a plain template, the corresponding Java class must have property names that match the plain template's slot names. Note, also, that just because two Java classes have an inheritance relationship doesn't mean that templates created from them will have the same relationship. You must explicitly declare all such relationships using extends. See the full documenation for deftemplate for details.
One final note about Java Beans used with Jess: Beans are often operating in a multithreaded environment, and so it's important to protect their data with synchronized blocks or synchronized methods. However, sending PropertyChangeEvents while holding a lock on the Bean itself can be dangerous, as the Java Beans Specification points out:
"In order to reduce the risk of deadlocks, we strongly recommend that event sources should avoid holding their own internal locks when they call event listener methods. Specifically, as in the example code in Section 6.5.1, we recommend they should avoid using a synchronized method to fire an event and should instead merely use a synchronized block to locate the target listeners and then call the event listeners from unsynchronized code." -- JavaBean Specification, v 1.0.1, p.31.Failing to heed this advice can indeed cause deadlocks in Jess.
5.4. Ordered facts
Most of the time, you will use unordered facts (or their cousins, shadow facts.) They are nicely structured, and they're the most efficient kind of fact in Jess. In some cases, though, slot names are redundant, and force you to do more typing than you'd like. For example, if a fact represents a single number, it seems silly to use an unordered fact like this:
(number (value 6))
What you'd like would be a way to leave out that redundant "value" identifier. Ordered facts let you do exactly that.
Ordered facts are simply Jess 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 working memory using the assert function, just as with unordered facts. If you add a fact to working memory whose head hasn't been used before, Jess assumes you want it to be an ordered fact and creates an appropriate template automatically. Alternatively, you can explicitly declare an ordered template using the ordered declaration with the deftemplate construct:
Jess> (deftemplate father-of "A directed association between a father and a child." (declare (ordered TRUE)))
The quoted string is a documentation comment; you can use it to describe the template you're defining. Although declaring ordered templates this way is optional, it's good style to declare all your templates.
Note that an ordered fact is very similar to an unordered fact with only one multislot. The similarity is so strong, that in fact this is how ordered facts are implemented in Jess. If you assert an ordered fact, Jess automatically generates a template for it. This generated template 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".
5.5. 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 working memory whenever a reset command is issued:
Jess> (deffacts my-facts "Some useless facts" (foo bar) (bar foo))
TRUE
Jess> (reset)
TRUE
Jess> (facts)
f-0 (MAIN::initial-fact) f-1 (MAIN::foo bar) f-2 (MAIN::bar foo) For a total of 3 facts in module MAIN.
5.6. How Facts are Implemented
Every fact, shadow or otherwise, corresponds to a single instance of the jess.Fact class. You can learn more about this class here. Templates are represented by instances of jess.Deftemplate, which you can read about here.