In this chapter you’ll...
- Learn to write rules
- Learn the difference between forward and backward chaining
- Learn how to partition your rules with modules
- Learn to probe working memory with queries
Now that you’ve learned how to populate the working memory, you can develop a knowledge base to go with it. This is the whole reason you’re here: The knowledge base is the collection of rules that make up a rule-based system. Rules can take actions based on the contents of working memory.
There are two main classes of rules in Jess: forward-chaining and backward-chaining rules. Forward-chaining rules are somewhat like if ... then statements in a procedural language, and they’re the most common and important kind of rule in Jess. Backward-chaining rules, on the other hand, don’t have a clear analogy in procedural programming. They are also similar to if ... then statements, but a backward-chaining rule actively tries to satisfy the conditions of its if part.
You can access working memory directly with queries. You can design queries to search working memory, to find specific facts, and to explore their relationships. Queries have a lot in common with rules—if you can write one, you know how to write the other. You’ll learn how to write and invoke queries in section 7.7.
A forward-chaining rule is something like an if ... then statement in a procedural language, but it is not used in a procedural way. Whereas if ... then statements are executed at a specific time and in a specific order, according to how the programmer writes them, a Jess rule’s then part can be executed whenever the if part is satisfied. This makes rules less obviously deterministic than a typical procedural program, because Jess decides the order in which to fire the rules. (See section 8.3’s discussion of 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.)
This section discusses the following functions and constructs:
- defrule— Defines a new rule
- ppdefrule— Pretty-prints a rule
- run— Begins firing activated rules from the agenda
- undefrule— Deletes a rule
- watch rules— Prints a diagnostic when a rule fires
- watch activations— Prints a diagnostic when a rule is activated
All Jess rules are defined using the defrule construct. The simplest possible rule looks like this:
Jess> (defrule null-rule "A rule that does nothing" => ) TRUE
The symbol null-rule is the name of the rule. A hyphen (-) is often used to separate words in a symbol. 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. There is also an undefrule function that can delete a rule by name.
The name is followed by an optional documentation string that describes the purpose of the rule. The symbol => (an equals sign followed by a greater-than sign) separates the rule’s left-hand side (LHS, or if part) from its right-hand side (RHS, or then part). The symbol => can thus be read as then. The previous rule has no conditions on its LHS and no actions on its RHS. It will therefore always execute, and it will accomplish nothing.
The following example uses two new arguments to the watch function, activations and rules (you used (watch facts) in chapter 6):
Jess> (watch facts) TRUE Jess> (watch activations) TRUE Jess> (watch rules) TRUE Jess> (reset) ==> f-0 (MAIN::initial-fact) ==> Activation: MAIN::null-rule : f-0 Jess> (run) FIRE 1 MAIN::null-rule f-0 1
The function call (watch activations) tells Jess to print a message whenever an activation record is placed on, or removed from, the agenda. An activation record associates a set of facts with a rule. It means the given set of facts matches the LHS of the given rule, and so the rule should be executed. In this case, because null-rule doesn’t specify a LHS, Jess has automatically made it conditional on the presence of the initial fact. You’ll recall from chapter 6 that the reset function places a fact (initial-fact) in working memory. This is one important role for (initial-fact): to serve as a trigger for rules with an empty LHS. You can see the change using the ppdefrule function, which pretty-prints a rule by re-creating its text from Jess’s internal representation:
Jess> (ppdefrule null-rule) "(defrule MAIN::null-rule \"A rule that does nothing\" (MAIN::initial-fact) =>)"
The return value of ppdefrule is a string, so when it is displayed to the console, the embedded quotation marks are escaped with a backslash character. It is important to note that all the work of pattern matching—comparing the LHSs of rules to a given fact—is done while that fact is being asserted. Because (initial-fact) is asserted by the reset function, null-rule is activated whenever the reset function is called, and that’s what happens here.
The function call (watch rules) tells Jess to print a message whenever a rule is fired. A rule is said to be fired when the actions on its RHS are executed. The run function tells Jess to start firing rules; no rules will fire except during a call to run. Jess’s rule engine then fires the rules on the agenda, one at a time, until the agenda is empty. run returns the number of rules fired—so 1 is printed in the previous example.
Now let’s look at a more complex rule:
Jess> (defrule change-baby-if-wet "If baby is wet, change its diaper." ?wet <- (baby-is-wet) => (change-baby) (retract ?wet)) TRUE
This rule again has two parts, separated by =>. The LHS consists of the pattern(baby-is-wet). The RHS consists of two function calls, to change-baby and retract. Note that the definition of change-baby isn’t shown here. Although you might at first find it hard to tell due to the Lisp-like syntax, the LHS of a rule consists of patterns that are used to match facts in the working memory, while the RHS contains function calls.
Let me say that again: The left-hand side of a rule (the if part) consists of patterns that match facts; they are not function calls. The right-hand side of a rule (the then clause) is 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))
Many novice Jess users write rules like this, intending (eq 1 1) to be interpreted as a function call. This rule will not fire just because the function call (eq 1 1) would evaluate to TRUE. Instead, Jess tries to find a fact in the working memory 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 conditional element, described in section 7.3.4.
The example rule, then, will be activated when the fact (baby-is-wet) appears in the working memory. When the rule fires, the function (change-baby) is called, and the (baby-is-wet) fact is retracted. This rule forms part of a complete program in listing 7.1.
Jess> (clear) TRUE Jess> (watch all) TRUE Jess> (reset) ==> f-0 (MAIN::initial-fact) TRUE Jess> (deffunction change-baby () (printout t "Baby is now dry" crlf)) TRUE Jess> (defrule change-baby-if-wet "If baby is wet, change its diaper." ?wet <- (baby-is-wet) => (change-baby) (retract ?wet)) change-baby-if-wet: +1+1+1+t TRUE Jess> (assert (baby-is-wet)) ==> f-1 (MAIN::baby-is-wet) ==> Activation: MAIN::change-baby-if-wet : f-1 <Fact-1> Jess> (run) FIRE 1 MAIN::change-baby-if-wet f-1 Baby is now dry <== f-1 (MAIN::baby-is-wet) 1
The watch all command in listing 7.1 tells Jess to print diagnostics for everything important that happens while this program runs. Many of the diagnostics in the listing are interesting. You first see how issuing the reset command again asserts the fact (initial-fact). Although this rule won’t need the initial fact, in most programs the initial fact will be needed by many rules, so you should always issue a reset command at some point before running a program.
When the rule is entered at the Jess prompt, you see the line +1+1+1+t. This result tells you something about Jess interprets the rule internally (see chapter 8 for more information). When the fact (baby-is-wet) is asserted, you see the diagnostic Activation: MAIN::change-baby-if-wet : f-1. This means Jess has noticed that all the LHS conditions of the rule change-baby-if-wet are met by the given list of facts—here the single fact f-1—and an activation record has been created. Note how the activation record associates the specific fact with the rule; this action will be important later.
Again, the rule doesn’t fire until you issue the run command. As soon as you enter (run), the activated rule fires. Because you entered the watch all command, Jess prints the diagnostic FIRE 1 MAIN::change-baby-if-wet f-1 to notify you of this action. The f-1 is a list of the facts that matched this rule’s LHS.
You then see the output of the rule’s RHS actions. First the function change-baby is called. Second, the fact f-1 is retracted. The variable ?wet is called a pattern binding; the <- operator stores a reference to the fact (baby-is-wet) in this variable, and the retract function can then access this variable on the rule’s RHS. Note, then, that rules cannot only react to the contents of working memory—they can change it. Thus one rule can put information into working memory, which in turn can cause another rule to fire.
The final number 1 is the number of rules that fired (the return value of the run command). The run function returns when there are no more activated rules to fire.
What would happen if you entered (run) again? Nothing. Jess activates a rule only once for a given working memory state. Once the rule has fired, it will not fire again for the same list of facts. You won’t change the baby again until a new baby-is-wet fact is asserted.
Jess’s working memory can hold JavaBeans as well as facts. Actually, you’ll recall that this isn’t quite correct: The working memory contains only facts; but some of those facts, called shadow facts, are stand-ins for JavaBeans. A shadow fact has a slot for every property of a JavaBean, and for dynamic shadow facts—defined using the definstance dynamic function—those slots track the contents of the Java-Bean’s properties in real time.
Therefore, everything about patterns in this chapter applies equally to patterns that match facts and to patterns that match JavaBeans. There’s no way to tell by looking at a pattern whether it’s intended to match deftemplate facts or shadow facts.
The baby-is-wet fact in the previous section didn’t have any slot data. Most facts do, however, and most patterns need to specify some particular set of slot values for the facts they match. These specifications are called constraints, because they constrain the values a slot can have in a fact that matches the pattern. A number of different kinds of constraints can be used to match slot data:
- Literal constraints— Specify exact slot values
- Variable constraints— Bind a matched value to a variable
- Connective constraints— Let you combine conditions to match A and B, or A or B
- Predicate constraints— Let you call a function to test for a match
- Return value constraints— Test for an exact match between a slot’s contents and the result of a function call
Literal slot values can be included in patterns as constraints. A pattern including a literal value matches only facts that include that value. In the following example, although both facts have the head letters, only the one with slot data that exactly matches the pattern activates the rule:
Remember that an ordered fact is implemented as an unordered fact with a single multislot named __data (a multislot, you’ll recall, can hold any number of items). You could therefore write the previous rule as
Jess> (assert (letters b c)) <Fact-0> Jess> (defrule literal-values (letters (__data b c)) =>) TRUE
and it would behave the same way (I asserted a letters fact first to emphasize that Jess only defines the implicit deftemplate for letters when it sees an ordered letters fact; the rule won’t be parsed correctly until this deftemplate exists). It’s important to keep this relationship in mind as you read this chapter; remember that everything that applies to ordered facts applies equally well to the multislots of unordered facts. The same goes for the regular slots of unordered facts, with the restriction that they can hold only one value at a time.
Finally, note that literal constraints have to match exactly; no conversions are done. Thus the floating-point literal 1.0 doesn’t match the integer 1, and the symbol xyz doesn’t match the string "xyz". This is a common source of problems when using shadow facts (see section 6.5).
If all the patterns of a rule had to be given literally, Jess would not be very powerful. However, patterns can also include variables and various kinds of predicates(comparisons and boolean functions), and can be modified by conditional elements. We’ll consider variables and predicates here and conditional elements in the following sections.
You can specify a variable instead of a literal value for any part of the slot data in a pattern. A variable matches any value in that position within the facts that match that pattern. For example, the rule
Jess> (defrule simple-variables (a ?x ?y) => (printout t "Saw 'a " ?x " " ?y "'" crlf))
is activated each time an ordered fact with head a having two fields is asserted: (a b c), (a 1 2), (a a a), and so forth. The variables thus matched on the LHS of a rule are available on the RHS of the same rule; you can think of them as the arguments to the rule’s RHS when it fires. For example, if the previous rule matched the fact (a b c), then when the rule fired, ?x would have the value b and ?y would have the value c. You can mix literal values and variables in the same pattern, of course.
The same variable can appear in more than one pattern in the same rule, or in several places within one pattern, or both. Every time the variable is used, it must match the same value. In listing 7.2, although two facts could match each pattern individually, only one pair can activate the rule: the fact (a 2) and (b 2).
Jess> (defrule repeated-variables (a ?x) (b ?x) => (printout t "?x is " ?x crlf)) TRUE Jess> (watch activations) TRUE Jess> (deffacts repeated-variable-facts (a 1) (a 2) (b 2) (b 3)) TRUE Jess> (reset) ==> Activation: MAIN::repeated-variables : f-2, f-3 TRUE Jess> (run) ?x is 2 1
Note that in Jess 6.1, you can’t use a variable to match the head of a fact or the name of a slot; these things must always be specified as literal values. This capability is planned for a future release, however.
Regular variables match exactly one value. Multifields can match any number of values—zero, one, or more. You write a multifield by preceding a variable name with the characters $?—for example, $?mf is a multifield. You can only use multifields in multislots. They can be used alone, in which case the multifield matches any number of values in that multislot, or in combination with regular variables or literal values. If you use multifields together with single values, the multifields expand to match everything not matched by the other values. For example, the pattern in this rule matches a shopping-cart fact with any number of values in the contents multislot:
(defrule any-shopping-cart (shopping-cart (contents $?items)) => (printout t "The cart contains " ?items crlf))
The pattern in this rule matches any shopping-cart fact with a contents slot that contains milk preceded by any number (zero or more) of items and followed by any number of additional items:
(defrule cart-containing-milk (shopping-cart (contents $?before milk $?after)) => (printout t "The cart contains milk." crlf))
As shown here, multifields are accessible on the RHS of the rules that use them in patterns (just as normal variables are). A multifield always contains the matched values as a list, even if it matches zero or one value. You can (and generally should, as a matter of style) leave the $ sign off a multifield when you refer to it on the RHS of a rule, because there it is acting as a normal variable.
You can match a field without binding it to a variable by omitting the variable name and using a question mark (?) as a placeholder. This is generally only useful as a way to specify that a multislot contains a certain arrangement of values without caring what those values are. For example, a pattern like (poker-hand ten ? ? ? ace) matches any poker-hand starting with a ten, ending with an ace, and containing a total of five cards. You can have blank multifields, too—just use bare $? characters.
If you match to a defglobal with a pattern like (score ?*x*), the match only considers the value of the defglobal when the fact is first asserted. Subsequent changes to the defglobal’s value will not invalidate the match—if the rule was activated based on the value of the defglobal, it stays activated even if the defglobal’s value changes. The match does not reflect the current value of the defglobal, but only the value at the time the matching fact was asserted.
Quite often, matching with a literal value or a variable isn’t enough. You might want a pattern to match if a client is located in either Boston or Hartford, for example, or you might want a pattern to match as long as the client is not from Bangor. You can write these patterns, and many others, using the connective constraints & (and), | (or), and ~ (not).
Any single constraint preceded by a tilde (~) matches the opposite of what the constraint would originally have matched. For example, the following pattern matches any client facts with a city slot that doesn’t contain Bangor:
(client (city ~Bangor))
This pattern matches clients that have purchased exactly two items, which must not be the same:
(client (items-purchased ?x ~?x))
The other connective constraints let you form groups of single constraints. Ampersands (&) represent logical and, and pipes (|) represent logical or. For example, this pattern matches any client that hails from Boston or Hartford:
(client (city Boston|Hartford))
And this one again matches any client not from Bangor, and in addition remembers the contents of city in the variable ?c:
(client (city ?c&~Bangor))
When you use several connective constraints together in a single expression, you should pay attention to operator precedence, or the way Jess groups the constraints together as it evaluates the expression. The ~ connective constraint has the highest precedence, followed by & and |, in that order. ~ always applies to the single constraint immediately following it, so the following (redundant) pattern matches all clients that are not from Bangor and are from Portland:
(client (city ~Bangor&Portland))
This pattern does not mean “all clients that are from neither Bangor nor Portland,” which would be written
(client (city ~Bangor&~Portland))
There are no grouping symbols that you can use with constraints—you can’t use parentheses to change their precedence. If you can’t express what you want using connective constraints, you can do it instead using predicate constraints, as described in the next section.
Literal constraints, variables, and connectives suffice for many situations, but there are some things they can’t express. Perhaps you want to match any shopping-cart that contains an odd number of items, or a client that lives in a city whose name contains more than 10 letters. Jess lets you specify these constraints, and virtually any other constraint you can imagine, using predicate functions. For our purposes, a predicate function is just a Boolean function—a function that returns TRUE or FALSE. You can use any predicate function as a constraint by preceding it with a colon (:). If you want to use the value of a slot as an argument to the function (and you almost always do), you should bind that value to a variable first, and then connect that binding to the function using the & connective:
Jess> (defrule small-order (shopping-cart (customer-id ?id) (contents $?c&:(< (length$ $?c) 5))) (checking-out-now ?id) => (printout t "Wouldn't you like to buy more?" crlf)) TRUE
You can use the and, or, and not predicate functions to express complex logical conditions. Although they are more verbose than the simple connective constraints, they are more powerful because you can group them into arbitrary structures. For example, this rule fires if a customer is checking out with more than 50 items, but his cart contains neither milk nor butter:
Jess> (defrule large-order-and-no-dairy (shopping-cart (customer-id ?id) (contents $?c& :(and (> (length$ $?c) 50) (not (or (member$ milk $?c) (member$ butter $?c)))))) (checking-out-now ?id) => (printout t "Don't you need dairy products?" crlf)) TRUE
Note that internally, Jess implements the | connective by transforming the whole pattern for that slot into predicate functions, and then using or to represent the |.
When evaluating a predicate constraint, Jess interprets any return value except FALSE as if it were TRUE. The member$ function returns FALSE if the given value is not a member of the list argument; otherwise it returns the position of the value in the list. Even though member$ never returns TRUE, it works perfectly well as a predicate function, because the non-FALSE values are interpreted as TRUE.
Often you’ll want to constrain the contents of slot to match the return value of a function. For example, if you wanted to find a pair of grocery items such that the price of one was exactly twice the price of another, you might use a predicate constraint like this:
(item (price ?x)) (item (price ?y&:(eq ?y (* ?x 2))))
(The eq function returns TRUE if the arguments are all equal, or FALSE otherwise.) Although this approach works, it’s not especially pretty. It would be more convenient to write this using a return value constraint. A return value constraint includes an arbitrary function, and the slot data must match whatever the function returns. When you’re writing a return value constraint, the function is preceded by an equals sign (=). You can rewrite the previous example using a return value constraint like so:
(item (price ?x)) (item (price =(* ?x 2)))
The return value constraint version is simpler because you don’t need the variable ?y or the call to eq.
In fact, pretty-printing a rule containing a return value constraint always shows that Jess has transformed it into an equivalent predicate constraint using eq, so the two forms are equivalent. Which one to use is a matter of taste.
To use retract, modify, or duplicate on a fact matched by the LHS of a rule, you need to pass a handle to the fact to the RHS of the rule. To do this, you use a pattern-binding variable:
Jess> (defrule pattern-binding ?fact <- (a "retract me") => (retract ?fact))
A reference to the jess.Fact object that activates this rule is bound to the variable ?fact when the rule is fired.
You can retrieve the name of a fact, its integer ID, and other useful data by calling the Java member functions of the jess.Fact class directly, like this:
Jess> (defrule call-fact-methods ?fact <- (initial-fact) => (printout t "Name is " (call ?fact getName) crlf) (printout t "Id is " (call ?fact getFactId) crlf)) TRUE Jess> (reset) TRUE Jess> (run) Name is MAIN::initial-fact Id is 0 1
Note that because pattern bindings have to refer to specific facts, you must be careful when using them with some of the grouping conditional elements described in the following sections. You can’t use them with not or test conditional elements, for example; and when using them with or and and conditional elements, you must be careful that the binding will apply to only one fact. Jess lets you write ambiguous bindings, but they may lead to errors at runtime, depending on how the patterns are matched. The next section presents some additional details on this issue.
We’ve just been looking at increasingly sophisticated ways to match the data within individual facts. Now we’ll look at ways to express more complex relationships between facts, and to qualify the matches for entire facts. Conditional elements(CEs) are pattern modifiers. They can group patterns into logical structures, and they can say something about the meaning of a match. There’s even one conditional element, test, that doesn’t involve matching a fact at all.
Before we begin, let me caution you that many of these conditional elements have the same names as predicate functions we looked at in the last section. There’s an and conditional element, and there’s an and predicate function. Although they may look similar, they’re entirely unrelated. The and predicate function operates on Boolean expressions, but the and conditional element operates on patterns. You can always tell which you’re dealing with by the context—predicate functions can appear only as constraints on slot data. The following are all of Jess’s conditional elements:
- and— Matches multiple facts
- or— Matches alternative facts
- not— Matches if no facts match
- exists— Matches if at least one fact matches
- test— Matches if a function call doesn’t evaluate to FALSE
- logical— Matching facts offer logical support to new facts
The LHS of every rule consists of a list of zero or more patterns. Each of those patterns must match for the whole LHS to match. You might recognize this as the intersection operation from formal logic. You can express the intersection of a group of patterns in Jess using the and conditional element. The entire LHS of every rule is enclosed in an implicit and.
Any number of patterns can be enclosed in a list with and as the head. The resulting pattern is matched if and only if all of the enclosed patterns are matched. The following rule matches only if (flaps-up) and (engine-on) both match:
Jess> (defrule ready-to-fly (and (flaps-up) (engine-on)) =>)
Of course, this rule would behave precisely the same way if the and CE was omitted, so by itself, and isn’t very interesting. Combined with or and not conditional elements, though, you can use the and CE to construct complex logical conditions.
Any number of patterns can be enclosed in a list with or as the head. The or CE matches if one or more of the patterns inside it matches. If more than one of the patterns inside the or matches, the entire or is matched more than once:
Jess> (clear) TRUE Jess> (deftemplate used-car (slot price) (slot mileage)) TRUE Jess> (deftemplate new-car (slot price) (slot warrantyPeriod)) TRUE Jess> (defrule might-buy-car ?candidate <- (or (used-car (mileage ?m&:(< ?m 50000))) (new-car (price ?p&:(< ?p 20000)))) => (assert (candidate ?candidate))) Jess> (assert (new-car (price 18000))) <Fact-0> Jess> (assert (used-car (mileage 30000))) <Fact-1> Jess> (run) 2
The rule fires twice: once for the new car and once for the used car. In this rule, only one of the two branches of the or conditional element will match at a time, but the rule can be activated separately as many times as there are facts to match. Each of the vehicles listed matches only one or the other of the branches. For some activations, the first branch of the or will match, and for others, it will be the second branch. Note that the variable ?candidate is bound to whatever fact matches the or in each particular activation. If might-buy-car’s RHS tried to modify the mileage slot of the used-car template, runtime errors would occur whenever ?candidate was bound to a new-car fact, because the new-car template doesn’t have such a slot.
If the RHS of a rule uses a variable defined by matching on the LHS of that rule, and the variable is defined by one or more branches of an or pattern but not all branches, then a runtime error may occur. For example, if the RHS of might-buy-car used the variable ?m (which is defined only when the rule matches a used-car fact), then when it fired in response to a new-car fact, you’d see an error message and Jess would stop firing rules.
The and group can be used inside an or group and vice versa. In the latter case, Jess rearranges the patterns so that there is a single or at the top level. For example, the rule
Jess> (defrule prepare-sandwich (and (or (mustard) (mayo)) (bread)) =>)
is automatically rearranged as follows:
Jess> (defrule prepare-sandwich (or (and (mustard) (bread)) (and (mayo) (bread))) =>)
Jess rearranges the patterns of any rule that has or conditional elements in it so that in the end, there is at most one or per rule, and it appears at the top level. Jess may use DeMorgan’s rules to accomplish this result. DeMorgan’s rules are a set of two formulas that describe legal ways of substituting logical expressions. Written in Jess syntax, they can be stated as follows:
(not (or (x) (y))) is the same as (and (not (x)) (not (y))) (not (and (x) (y))) is the same as (or (not (x)) (not (y)))
Jess does this rearrangement so that it can form subrules, which are the topic of the next section.
A rule containing an or conditional element with n branches is precisely equivalent to n rules, each of which has one of the branches on its LHS. In fact, this is how Jess implements the or conditional element: Jess internally divides the rule, generating one rule for each branch. Each of these generated rules is a subrule. For a rule named rule-name, the first subrule is also named rule-name, the second is rule-name&1, the third is rule-name&2, and so on. Each of these subrules is added to the Rete network individually. If you execute the (rules) command, which lists all the defined rules, you will see each of the subrules listed separately. If you use the ppdefrule function to see a pretty-print representation of a subrule, you will see only the representation of that particular subrule. Note that because & is a token delimiter in the Jess grammar, you can only refer to a subrule with an ampersand in the name by placing the whole name in quotes—for example, (ppdefrule "rule-name&6").
Jess knows that the subrules created from a given rule are related. If the original rule is removed (either using undefrule or implicitly by defining a new rule with the same name as an existing one), every subrule associated with that rule is also removed.
A note regarding subrules and efficiency: You’ll learn in chapter 8 that similar patterns are shared between rules in the Rete network, avoiding duplicated computation. Therefore, splitting a rule into subrules does not mean the amount of pattern-matching work is increased; much of the splitting may indeed be undone when the rules are compiled into the network.
On the other hand, keep the implementation in mind when you define your rules. If an or conditional element is the first pattern on a rule, all the subsequent pattern-matching on that rule’s LHS won’t be shared between the subrules, because sharing occurs only as far as two rules are similar reading from the top down. Placing or conditional elements near the end of a rule leads to more sharing between the subrules, and thus more efficient pattern matching.
Finally, I should mention that although subrules will probably always be part of the implementation of the or conditional element in Jess, it is likely that they will no longer be user-visible at some time in the future.
You may have heard the saying “two wrongs don’t make a right” when you were growing up. How can the opposite of the opposite of something not be the same as the original thing? Well, as it turns out, it’s quite often not. Such is the case in real-world logic: The concept of negation is a tricky thing. It’s tricky in Jess, too.
Imagine that you want a rule to fire when no red cars are available. Your first try might look something like this:
Jess> (defrule no-red-cars (auto (color ~red)) =>)
But this rule fires for each car that is not red. If there are no cars at all, it won’t fire. This result isn’t the same as firing when there are no red cars.
Luckily, Jess has the not conditional element. Most patterns can be enclosed in a list with not as the head. In this case, the pattern is considered to match if a fact (or set of facts) that matches the pattern is not found. For example, this rule will fire if there are no cars at all, or if there are only blue cars, but not if there are any red ones:
Jess> (defrule no-red-cars-2 (not (auto (color red))) =>)
Because a not pattern matches the absence of a fact, it cannot define any variables that are used in subsequent patterns on the LHS. You can introduce variables in a not pattern as long as they are used only within that pattern:
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; again, because it doesn’t match an actual fact, there would be no fact to bind to the variable.
Now, here comes the tricky part I alluded to earlier. You already know that pattern matching is driven by facts being asserted—the matching computation happens during the assert, definstance, modify, duplicate, or reset function that creates the fact. Because a not CE matches the absence of a fact, when can it be evaluated? The answer is that a not CE is evaluated only in these cases:
- When a fact matching it is asserted (in which case the pattern match fails)
- When a fact matching it is removed (in which case the pattern match succeeds)
- 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 first pattern in an and group, or the first pattern on a given branch of an or group, the pattern (initial-fact) is inserted before the not to become this important preceding pattern. Therefore, the initial fact created by the reset command is necessary for the proper functioning of many 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.
The not CE can be used in arbitrary combination with the and and or CEs. You can define complex logical structures this way. For example, suppose you want a rule to fire once, and only once, if for every car of a given color, there exists a bus of the same color. You could express that as follows:
Jess> (defrule forall-example (not (and (car (color ?c)) (not (bus (color ?c))))) =>)
Decoding complex logical expressions is easier if you start from the inside and work your way out. The innermost pattern here is (bus (color ?c)), which matches any bus fact. The not around that matches only when there are no bus facts. The (car (color ?c)) pattern matches any car facts, and the and groups these two patterns together. The entire and thus matches when there is a car, but no bus of the same color. Putting the and group into the outermost not means the whole pattern matches only when the and doesn’t; thus the whole thing can be translated as “It is not true that for some color ?c, there is a car of that color but no bus of that same color.”
In the next section we’ll look at another interesting way to combine not CEs into more complex groups.
You can nest multiple not CEs to produce some interesting effects. Two nots nested one inside the other are so useful that there’s a shorthand notation: the exists CE. 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—which is precisely the meaning of two nested nots. The exists CE 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-an-honest-man (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. The exists CE is implemented as two nested not CEs; that is, (exists (A)) is exactly the same as (not (not (A))). Therefore, you can’t bind any variables in an exists CE for use later in the rule, and you also can’t use pattern bindings with exists.
A pattern with test as the head is special; the body consists not of a pattern to match against the working memory but of a Boolean function. The result determines whether the pattern matches. A test pattern fails if and only if the function evaluates to the symbol FALSE; if it evaluates to TRUE, the pattern succeeds. For example, suppose you wanted to find people whose age is less than 30 years old:
Jess> (deftemplate person (slot age)) TRUE Jess> (defrule find-trustworthy-people-1 (person (age ?x)) (test (< ?x 30)) => (printout t ?x " is under 30!" crlf)) TRUE
Because a test CE, like a not CE, doesn’t match an actual fact, its implementation is similar to the way not is implemented. A test CE is evaluated every time the preceding pattern on the rule’s LHS is evaluated, just like a not. Therefore the following rule is equivalent to the previous one:
Jess> (defrule find-trustworthy-people-2 (person (age ?x&:(< ?x 30))) => (printout t ?x " is under 30!" crlf))
Which form you use here is mostly a matter of taste. I tend to use the test CE only for long or complex functions that would be hard to read if they were written as predicate constraints. Of course, the test CE can also be used to write tests that are unrelated to any facts:
(import java.util.Date) (defrule fire-next-century (test ((new Date) after (new Date "Dec 31 2099"))) => (printout t "Welcome to the 22nd century!" crlf))
For rules like this, in which a test CE is the first pattern on the LHS, or the first pattern in an and CE, or the first pattern in any branch of an or CE, Jess inserts the pattern (initial-fact) to serve as 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. The rule fire-next-century won’t fire until reset is called after the twenty-second century begins.
The test and not conditional elements may be combined, so that
(not (test (eq ?x 3)))
is equivalent to
(test (neq ?x 3))
The conditional elements we’ve looked at so far affect how a rule matches working memory. There is one conditional element we haven’t covered yet, and it’s unusual in that instead of affecting how a rule matches, it affects what happens when a rule fires.
When you turn on your kitchen faucet, you expect water to come out (if it doesn’t, you’ve got a plumbing problem). When you turn off the faucet, the flow of water stops as a result. This kind of relationship is called a logical dependency—the water flowing is logically dependent on the faucet being open. To express this idea in Jess, you could write the following two rules:
Jess> (defrule turn-water-on (faucet open) => (assert (water flowing))) TRUE Jess> (defrule turn-water-off (not (faucet open)) ?water <- (water flowing) => (retract ?water)) TRUE
Given these two rules, asserting (faucet open) will automatically cause (water flowing) to be asserted as well, and retracting (faucet open) will retract (water flowing)—if you call run so the rules can fire, of course. The fact (water flowing) can therefore be said to be logically dependent on (faucet open).
Writing two rules to express the one idea of logical dependency gets the job done, but there is an easier way. The logical conditional element lets you specify these logical dependencies among facts more concisely. All the facts asserted on the RHS of a rule are logically dependent on any facts that matched a pattern inside a logical CE on that rule’s LHS. If any of the matches later become invalid—for instance, because one of the facts is deleted—the dependent facts are retracted automatically. In the simple example in listing 7.3, the (water-flowing) fact is again logically dependent on the (faucet-open) fact, so when the latter is retracted, the former is removed, too.
If fact 1 is logically dependent on fact 2, you can also say that fact 1 “receives logical support from” fact 2. A fact may receive logical support from multiple sources—it may be asserted multiple times with a different set of logical supports each time. Such a fact isn’t automatically retracted unless each of its logical supports is removed. If a fact is asserted without explicit logical support, it is said to be unconditionally supported. If an unconditionally supported fact also receives explicit logical support, removing that support will not cause the fact to be retracted. You can find out what logical support a fact is receiving with the dependencies function. The dependents function tells you what facts are dependent on another given fact. Both functions take either a single fact object or an integer fact ID as an argument.
If one or more logical CEs appear in a rule, they must be the first patterns in that rule; a logical CE cannot be preceded in a rule by any other kind of CE. You can use the logical CE together with all the other CEs, including not and exists. A fact can thus be logically dependent on the nonexistence of another fact or on the existence of some category of facts in general.
Shadow facts from definstances are no different than other facts with regard to the logical CE. Shadow facts can provide logical support and can receive logical support.
The rules you’ve seen so far have been forward-chaining rules; as I’ve said, that means the rules are treated as if ... then statements, with the engine simply executing the RHSs of activated rules. Some rule-based systems, notably Prolog and its derivatives, support backward chaining. In a backward-chaining system, rules are still if ... then statements, but the engine actively tries to make rules fire. If the if clause of one rule is only partially matched and the engine can determine that firing some other rule would cause it to be fully matched, the engine tries to fire that second rule. This behavior is often called goal seeking.
As an example, think about the ways Sherlock Holmes might solve a mystery. He has a collection of evidence (a handkerchief, a fingerprint, a dead body) and can proceed in two different ways. First, he can draw conclusions from the available evidence, adding his conclusions to the available information, and continue until he’s found a link between the evidence and the crime. This is a forward-chaining method. Alternatively, he can start from the circumstances of the crime, form a hypothesis about how it happened, and then search for clues that support this hypothesis. This latter technique is an example of backward chaining. Holmes generally used both techniques in combination to solve a mystery; as a Jess programmer, you’ll do the same.
Jess supports both forward and backward chaining, but Jess’s version of backward chaining is not transparent to the programmer. You have to declare which kinds of facts can serve as backward-chaining triggers, and only specific rules you define can be used in backward chaining. In truth, Jess’s reasoning engine is strictly a forward-chaining engine, and so backward chaining is effectively simulated in terms of forward-chaining rules. Still, the simulation is quite effective, and Jess’s backward-chaining mechanism has many useful applications. You will apply it in several of the systems you develop later in this book.
Backward chaining is often used as a way to pull required data into Jess’s working memory from a database on demand. In the example given here, backward chaining is used to avoid computing the factorial of a number more than once (the factorial of an integer is the product of every integer between 1 and the number itself, inclusive; for large numbers this value can be expensive to compute). You use the deftemplate factorial to store computed factorials. The fact (factorial 5 125) signifies that the factorial of 5 is 125. Figure 7.1 shows how this example works: The rule print-factorial-10 won’t fire unless a fact giving the factorial of 10 is present. Because factorial has been registered for backward chaining with the do-backward-chaining function, Jess automatically asserts the fact (need-factorial 10 nil). This fact matches the need-factorial pattern in the do-factorial rule, which fires and asserts the fact (factorial 10 3628800). Finally, this fact activates the print-factorial-10 rule, which fires and prints its output.
Jess> (do-backward-chaining factorial) TRUE
If the template is unordered—if it is explicitly defined with a deftemplate or defclass construct—then you must define it before calling do-backward-chaining. You can use do-backward-chaining on ordered deftemplates before they are created, however.
Once you have declared your reactive deftemplates, you can define rules with patterns that match facts of the corresponding types. Note that you must call do-backward-chaining before defining any rules that use the template.
This rule prints the factorial of 10, assuming a fact recording this information exists:
Jess> (defrule print-factorial-10 (factorial 10 ?r1) => (printout t "The factorial of 10 is " ?r1 crlf)) TRUE
Patterns that match backward-chaining reactive deftemplates are called goals. When the rule compiler sees a goal pattern, it rewrites the rule and inserts some special code into the internal representation of the rule’s LHS. If, when the rule engine is reset, there are no matches for this pattern, the code asserts a fact into working memory that looks like this:
(need-factorial 10 nil)
The head of the fact is constructed by taking the head of the reactive pattern and adding the prefix need-. These need-x facts are called goal-seeking or trigger facts. This particular trigger fact means that another fact (factorial 10 ?) is needed to satisfy some rule. Jess got the number 10 directly from the pattern in print-factorial-10; nil is a placeholder that means “any value.”
Now, let’s write a rule that calculates the factorial of a number when it is needed. The rule should directly match the need-factorial trigger facts:
Jess> (defrule do-factorial (need-factorial ?x ?) => ;; compute the factorial of ?x in ?r (bind ?r 1) (bind ?n ?x) (while (> ?n 1) (bind ?r (* ?r ?n)) (bind ?n (- ?n 1))) (assert (factorial ?x ?r))) TRUE
The rule compiler rewrites rules like this too: It adds a negated match for the (factorial ?x ?) pattern to the rule’s LHS, so the rule won’t fire if both the goal fact and the corresponding goal-seeking fact are both present.
The end result is that you can write rules that match on factorial facts, 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 backward chaining! Note that any needed factorial facts are created only once, so the expensive computation need not be repeated. Often, avoiding redundant computation is one of the main benefits of backward chaining.
Jess chains backward through any number of reactive patterns. In the example in listing 7.4, imagine you have a database that allows you to look up the price of an item given its item number, or the item number given its name. To find the price given the name, you need to do two separate queries. When the price-check fact is first asserted, none of the rules can be activated. Jess sees that price-check could be activated if there were an appropriate price fact, so it generates the trigger (need-price waffles nil). This matches part of the LHS of rule find-price, but this rule cannot be activated because there is no item-number fact. Jess therefore creates a (need-item-number waffles nil) request. This matches the LHS of the rule find-item-number, which fires and asserts something like (item-number waffles 123). This fact activates find-price, which fires and asserts (price waffles "$1.99"), thereby activating rule price-check, which then fires. The price is reported. Each of the rules has fired once. The definitions of the functions fetch-price-from-database and fetch-number-from-database are not shown; they are presumably written in Java using JDBC.
Jess> (clear) TRUE Jess> (do-backward-chaining item-number) TRUE Jess> (do-backward-chaining price) TRUE Jess> (defrule price-check (do-price-check ?name) (price ?name ?price) => (printout t "Price of " ?name " is " ?price crlf)) TRUE Jess> (defrule find-price (need-price ?name ?) (item-number ?name ?number) => (bind ?price (fetch-price-from-database ?number)) (assert (price ?name ?price))) TRUE Jess> (defrule find-item-number (need-item-number ?name ?) => (bind ?number (fetch-number-from-database ?name)) (assert (item-number ?name ?number))) TRUE Jess> (reset) TRUE Jess> (assert (do-price-check waffles)) <Fact-1> Jess> (run) Price of waffles is $1.99 3
You can wrap a special conditional element, (explicit), around a pattern to inhibit backward chaining on an otherwise reactive pattern. explicit can be used in any combination with all other conditional elements.
Most rule-based systems consist of dozens if not hundreds of rules. While such a program is running, at any one time a large number of rules may be simultaneously activated. How does Jess decide which rule to fire next? Read on to find out.
In section 7.1, you used (watch activations) and (watch rules) to observe the operation of a simple rule. In particular, you learned that a rule is activated when its LHS matches working memory, but it won’t immediately fire. The agenda is the list of rules that have been activated but haven’t fired yet. For some applications, the agenda never contains more than one activated rule, and so managing the agenda isn’t a very interesting topic. But in most applications, the agenda contains multiple rules at once. When this is the case, managing the agenda becomes important. In this section, we’ll study how Jess chooses which rule to fire next from among all the activated rules on the agenda, and how you can influence this choice.
A typical rule-based system may contain hundreds or thousands of rules. It’s very likely that at any given moment, more than one rule is activated. The set of activated rules that are eligible to be fired is called the conflict set, and the process of putting the rules in firing order is called conflict resolution. The output of the conflict-resolution process is the ordered list of activations called the agenda. You can see this ordered list of activated, but not yet fired, rules with the agenda function.
Conflict resolution in Jess is controlled by pluggable conflict-resolution strategies. Jess comes with two strategies: depth (the default) and breadth. You can set the current strategy with the set-strategy command. Using (set-strategy depth) causes the most recently activated rules to fire first, and (set-strategy breadth) makes rules fire in activation order—the most recently activated rules fire last. In many situations, the difference does not matter, but for some problems the conflict-resolution strategy is important. Although the default strategy is intuitive and correct in most situations, it runs into trouble if every rule that fires activates another rule. The oldest activations then get pushed far down the agenda and never get a chance to fire. The breadth strategy avoids this problem, but the “first-in, first-out” firing order can be confusing.
You can write your own strategies in Java by implementing the jess.Strategy interface and then calling set-strategy with the name of your class as the argument. The Strategy interface has a single nontrivial method compare that compares two activations and returns -1, 1, or 0 to signify that the first activation, the second activation, or either one should fire first.
The conflict-resolution strategy determines how activations are ordered based on when they are added to the agenda. Sometimes, though, you may find that you want to fine-tune the ordering a bit. You can use salience to accomplish this.
Sometimes you may find that a particular rule should be treated as a special case by the conflict-resolution strategy. A rule that reports a security breach might need to fire immediately, regardless of what else is on the agenda. On the other hand, a rule that cleans up unused facts might only need to run during the idle time when no other rules are activated. You can tell the conflict resolver to treat these rules specially using rule salience.
Each rule has a property called salience that acts as a priority setting for that rule. Activated rules of the highest salience always fire first, followed by rules of lower salience. Within a set of rules with identical salience, the order is determined as described in the previous section. You can use a salience declaration to set the salience of a rule:
Jess> (defrule defer-exit-until-agenda-empty (declare (salience -100)) (command exit-when-idle) => (printout t "exiting..." crlf)) TRUE
This rule won’t fire until no other rules of higher salience are on the agenda. 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 0, so if this is the only rule with an explicit salience value, it will not fire until the agenda is empty.
You can specify salience values using literal integers, global variables, or function calls. How the salience values are evaluated depends on the current value of the salience evaluation method. These values are as follows:
- when-defined— (Default.) A fixed salience value is computed when the rule is defined.
- when-activated— The salience of a rule is reevaluated each time the rule is activated.
- every-cycle— The salience value of every rule on the agenda is recomputed after every rule firing. Evaluating every-cycle is very computationally expensive and isn’t used much.
You can query or set the salience evaluation method with the set-salience-evaluation and get-salience-evaluation functions.
Note that extensive use of salience is generally discouraged, for two reasons. First, use of salience has a negative impact on performance, at least with the built-in conflict-resolution strategies. Second, it is considered bad style in rule-based programming to try to force rules to fire in a particular order. If you find yourself using salience on most of your rules, or if you are using more than two or three different salience values, you probably need to reconsider whether you should be using a rule-based approach to your problem. If you want strict control over execution order, then you’re trying to implement a procedural program. Either change your rules to be less sensitive to execution order, or consider implementing your algorithm as one or more deffunctions or as Java code. Alternatively, you might consider structuring your program using modules.
A typical rule-based system can easily include hundreds of rules, and a large one can contain many thousands. Developing such a complex system can be a difficult task, and preventing such a multitude of rules from interfering with one another can be hard too.
You might hope to mitigate the problem by partitioning a rule base into manageable chunks. The defmodule construct lets you divide rules and facts into distinct groups called modules. Modules help you in two ways: First, they help you physically organize large numbers of rules into logical groups. The commands for listing constructs (rules, facts, and so on) let you specify the name of a module and can then operate on one module at a time. Second, modules provide a control mechanism: The rules in a module fire only when that module has the focus, and only one module can be in focus at a time (you’ll learn about module focus in section 7.6.3).
We’ll discuss the following functions and constructs in this section:
- clear-focus-stack— Empties the focus stack
- defmodule— Defines a new module
- focus— Sets the focus module
- get-current-module— Returns the current module
- get-focus-stack— Returns the focus stack’s contents as a list
- list-focus-stack— Displays the focus stack’s contents
- pop-focus— Pops a module from the focus stack
So far in this book, you haven’t explicitly used modules. If you don’t specify a module by name when defining a rule or template, it belongs by default to the current module. If you never explicitly define any modules, the current module is always the main module, which is named MAIN. All the constructs you’ve seen so far have been defined in MAIN, and therefore are often preceded by MAIN:: when displayed by Jess.
You can define a new module using the defmodule construct:
Jess> (defmodule WORK) TRUE
You can then place a deftemplate, defrule, or deffacts into a specific module by qualifying the name of the construct with the module name:
Jess> (deftemplate WORK::job (slot salary)) TRUE Jess> (list-deftemplates WORK) WORK::job For a total of 1 deftemplates.
Once you have defined a module, it becomes the current module:
Jess> (get-current-module) WORK Jess> (defmodule COMMUTE) TRUE Jess> (get-current-module) COMMUTE
If you don’t specify a module, all deffacts, templates, and rules you define automatically become part of the current module:
Jess> (deftemplate bus (slot route-number)) TRUE Jess> (defrule take-the-bus ?bus <- (bus (route-number 76)) (have-correct-change) => (get-on ?bus)) TRUE Jess> (ppdefrule take-the-bus) "(defrule COMMUTE::take-the-bus ?bus <- (COMMUTE::bus (route-number 76)) (COMMUTE::have-correct-change) => (get-on ?bus))"
Note that the implied deftemplate have-correct-change was created in the COMMUTE module, because that’s where the rule was defined. You can set the current module explicitly using the set-current-module function.
A module defines a namespace for templates and rules. This means two different modules can each contain a rule with a given name without conflicting—for example, rules named MAIN::initialize and COMMUTE::initialize could be defined simultaneously and coexist in the same program. Similarly, the templates COMPUTER::bus and COMMUTE::bus could both be defined. Obviously, then, Jess needs a way to decide which template the definition of a rule or query is referring to.
When Jess is compiling a rule, query, or deffacts definition, it looks for templates in three places, in order:
- If a pattern explicitly names a module, only that module is searched.
- If the pattern does not specify a module, then the module in which the rule is defined is searched first.
- If the template is not found in the rule’s module, the module MAIN is searched last. Note that this makes the MAIN module a sort of global namespace for templates.
The example in listing 7.5 illustrates each of these possibilities. In this example, three deftemplates are defined in three different modules: MAIN::mortgage-payment, WORK::job, and HOME::hobby. Jess finds the WORK::job template because the rule is defined in the WORK module. It finds the HOME::hobby template because it is explicitly qualified with the module name. And the MAIN::mortgage-payment template is found because the MAIN module is always searched as a last resort if no module name is specified.
Jess> (clear) TRUE Jess> (assert (MAIN::mortgage-payment 2000)) <Fact-0> Jess> (defmodule WORK) TRUE Jess> (deftemplate job (slot salary)) TRUE Jess> (defmodule HOME) TRUE Jess> (deftemplate hobby (slot name) (slot income)) TRUE Jess> (defrule WORK::quit-job (job (salary ?s)) (HOME::hobby (income ?i&:(> ?i (/ ?s 2)))) (mortgage-payment ?m&:(< ?m ?i)) => (call-boss) (quit-job)) TRUE Jess> (ppdefrule WORK::quit-job) "(defrule WORK::quit-job (WORK::job (salary ?s)) (HOME::hobby (income ?i&:(> ?i (/ ?s 2)))) (MAIN::mortgage-payment ?m&:(< ?m ?i)) => (call-boss) (quit-job))"
Commands that accept the name of a construct as an argument (like ppdefrule, ppdeffacts, and so on) search for the named construct as described earlier. Note that many of the commands that list constructs (such as facts, list-deftemplates, and rules) accept a module name or * as an optional argument. If no argument is specified, these commands operate on the current module. If a module name is given, they operate on the named module. If * is given, they operate on all modules (see appendix A for full descriptions of all Jess functions and the arguments they accept).
You’ve learned how modules provide a kind of namespace facility, allowing you to partition a rule base into manageable chunks. You can also use modules to control execution. In general, although any Jess rule can be activated at any time, only rules in the focus module will fire. Note that the focus module is independent from the current module discussed earlier.
Initially, the module MAIN has the focus, so only rules in the MAIN module can fire:
Jess> (defmodule DRIVING) TRUE Jess> (defrule get-in-car => (printout t "Ready to go!" crlf)) TRUE Jess> (reset) TRUE Jess> (run) 0
In this example, the rule doesn’t fire because the DRIVING module doesn’t have the focus. You can move the focus to another module using the focus function (which returns the name of the previous focus module):
Jess> (focus DRIVING) MAIN Jess> (run) Ready to go! 1
Jess maintains a focus stack containing an arbitrary number of modules. The focus command pushes the new focus module onto the top of this stack; the focus module is, by definition, the module on top of the stack. When there are no more activated rules in the focus module, it is popped from the stack, and the next module underneath becomes the focus module. The module MAIN is always at least on the bottom of the stack; it can also be explicitly pushed onto the focus stack.
You can manipulate the focus stack directly with the functions pop-focus, clear-focus-stack, list-focus-stack, and get-focus-stack. pop-focus removes the focus module from the focus stack, so that the next module on the stack becomes active. clear-focus-stack removes all the modules from the focus stack. The other functions let you examine the contents of the focus stack.
Rule bases are commonly divided into modules along functional lines. For example, you might put all your input-gathering rules into one module, your data-processing rules into another, and your reporting rules into a third. Then, changing the focus from input, to processing, to output represents a natural progression through well-defined phases of your application’s execution.
When a rule that declares the auto-focus property is activated, its module automatically gets the focus, as illustrated in listing 7.6. In this example, the rule crash fires even though its module PROBLEMS didn’t have the focus and the agenda of the previous focus module DRIVING was not empty. Modules with auto-focus rules make great background tasks in conjunction with using return from a rule, as described next.
Jess> (defmodule PROBLEMS) TRUE Jess> (defrule crash (declare (auto-focus TRUE)) (DRIVING::me ?location) (DRIVING::other-car ?location) => (printout t "Crash!" crlf) (halt)) TRUE Jess> (defrule DRIVING::travel ?me <- (me ?location) => (printout t ".") (retract ?me) (assert (me (+ ?location 1)))) TRUE Jess> (assert (me 1)) <Fact-1> Jess> (assert (other-car 4)) <Fact-2> Jess> (focus DRIVING) MAIN Jess> (run) ...Crash! 4
If the function return is called from a rule’s RHS, the execution of that rule’s RHS is immediately terminated. Furthermore, the current focus module is popped from the focus stack. This suggests that you can call a module like a subroutine. You can call a module from a rule’s RHS using focus, and the module can return from the call using return. Alternatively, a module can act as a kind of background process or periodic task by using auto-focus rules to wake itself and return to put itself back to sleep.
Both forward- and backward-chaining rules can only react to the contents of working memory. They are passive in the sense that they wait for facts to appear before they can take action, and then nothing happens until they get to the top of the agenda. Sometimes you may want to take a more active stance and deliberately search through working memory to find particular information. Jess lets you do this easily, as you’ll see in the next section.
Jess’s working memory is a lot like a relational database. Each deftemplate is like a relation—a table in the database. The individual slots are the columns of the tables. If you’re familiar with industrial-strength relational databases, you’re probably aware of database triggers, which are a lot like forward-chaining rules attached to a database that fire when the data matches some criterion. You can apply rules to relational databases, so it’s a reasonable question to ask whether you can make queries against the working memory of a rule-based system. Jess offers the defquery construct, which lets you do just that.
A defquery is a special kind of rule with no RHS. Jess controls when regular rules fire, but queries are used to search the working memory under direct program control. A rule is activated once for each matching set of facts, whereas a query gives you a java.util.Iterator of all the matches. An example should make this clear. Suppose you have defined the query find-affordable-gifts:
Jess> (deftemplate gift (slot name) (slot price)) TRUE Jess> (defquery find-affordable-gifts "Finds all gifts in a given price range" (declare (variables ?lower ?upper)) (gift (price ?p&:(and (> ?p ?lower) (< ?p ?upper))))) TRUE
The pattern here matches all the gifts whose price slot holds a number between ?lower and ?upper.
Now you define some facts, including some that match the criterion and some that don’t:
Jess> (deffacts catalog (gift (name red-scarf) (price 20)) (gift (name leather-gloves) (price 35)) (gift (name angora-sweater) (price 250)) (gift (name mohair-sweater) (price 99)) (gift (name keychain) (price 5)) (gift (name socks) (price 6)) (gift (name leather-briefcase) (price 300))) TRUE
You can invoke the query to find the perfect gift using concrete upper and lower price limits:
Jess> (reset) TRUE Jess> (bind ?it (run-query find-affordable-gifts 20 100)) <External-Address:java.util.AbstractList$Itr> Jess> (while (?it hasNext) (bind ?token (call ?it next)) (bind ?fact (call ?token fact 1)) (bind ?name (fact-slot-value ?fact name)) (printout t ?name crlf)) leather-gloves mohair-sweater FALSE
Here you’re looking for gifts between $20 and $100, and the query finds mohair-sweater and leather-gloves.
Let’s break down this code to see what it’s doing. As previously stated, (run-query) returns the query results as a Java java.util.Iterator object. The Iterator interface has a method next() that you call to retrieve each individual result; it also has a hasNext() method that returns true as long as there are more results to return. That explains the (while (?it hasNext) ...(call ?it next)) control structure; it steps through each of the results returned by the query.
Each individual result is a jess.Token object. A Token is just a collection of jess.Fact objects; each Token holds one match for the query. You call the fact() method of jess.Token to retrieve the individual jess.Fact objects within the Token. Each match begins with an extra fact, a query trigger fact that initiates the matching process; it is asserted by the run-query command (this fact is retracted automatically after the query is run). Hence the argument to the call to fact() is 1, not 0. Once you have the right fact, you use the fact-slot-value function to extract the contents of the name slot. Printing the name slot of each fact leads to the output shown earlier.
The defquery construct can use virtually all the same features that defrule LHSs can, including all the special conditional elements described in this chapter. The function ppdefrule can also pretty-print queries. Jess treats a defquery as a special kind of defrule in many contexts; for instance, the rules command lists defquerys as well as defrules.
As you can see, the run-query function lets you pass parameters to a query; you passed numbers representing the upper and lower limits of a price range to the find-affordable-gifts query. Let’s examine this process a little more closely.
You might have already realized that two different kinds of variables can appear in a query: those that are internal to the query, like ?p in find-affordable-gifts, 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 this syntax:
(declare (variables ?x ?y ...))
When you invoke a query using the run-query function, you must supply exactly as many variables as are listed in the variables declaration. Some queries may not have any external variables; in this case, the variables declaration is optional.
When Jess compiles a defquery, it inserts an extra pattern as the first one in the query. This first pattern is of the form
(__query-trigger-name ?x ?y ...)
where name is the name of the query and ?x, ?y, and so on are the variables named in the variables declaration for the query. run-query works by asserting a fact to match this pattern, using the arguments you supply to instantiate the variables. This fact completes any pending matches of the defquery’s LHS, and run-query collects these matches and returns them.
To obtain just the number of matches for a query, rather than a full Iterator over all the matches, you can use the count-query-results function. This function accepts the same arguments as run-query but returns an integer specifying the number of matches.
It can be convenient to use queries as triggers for backward chaining. For example, look back at the backward-chaining example in section 7.4. If you were writing a deffunction that needed to use factorials, that deffunction might want to use a defquery to fetch the ones that are already available from working memory, rather than recomputing them. The backward-chaining rules would then compute missing values.
For this technique to be useful, (run) must somehow be called while the query is being evaluated, to allow the backward chaining to occur. Facts generated by rules fired during this run may appear as part of the query results.
By default, no rules fire while a query is being executed. If you want to allow backward chaining to occur in response to a query, you can include the max-background-rules declaration in that query’s definition. For example, this query allows a maximum of five rules to fire while it is being executed:
Jess> (defquery find-factorial (declare (max-background-rules 5) (variables ?arg)) (factorial ?arg ?))
You can define rules to take action based on the contents of Jess’s working memory, and you can write queries to investigate it procedurally. Both rules and queries can use constraints (conditions on the slot data of facts) and conditional elements(relationships between facts) to express specify detailed requirements on working memory elements.
You can write both forward- and backward-chaining rules. Roughly, you can say that forward-chaining rules discover the conclusions that can be drawn from an existing set of facts, and backward-chaining rules search for the premises from which existing facts can be derived. You can also write queries to probe working memory directly.
In real systems, many rules are activated simultaneously. Conflict resolution, or choosing which rule to fire next, is an important part of any rule-based system. Jess lets you influence conflict resolution in a number of ways: by setting the conflict-resolution strategy, by using salience, or by partitioning your rule base into modules.