home *** CD-ROM | disk | FTP | other *** search
- Chapter 10 Adding Structure
-
-
-
-
- In this chapter you'll see alternate ways to write loops and other program
- structures than the standard method of multiple rules. While these
- method do work, there is also an inherent danger because your program design
- can become very inefficient.
-
-
- If This Goes On
-
-
- CLIPS provides some programming structures that can be used on the RHS. These
- structures are the while and if then else that are also found
- in modern high-level languages such as Ada and Pascal.
- The basic (if then else) has the form
-
- (if (<fun> <<arg>>)
- then
- (<<actions>>)
- else
- (<<actions>>)
-
- where <fun> stands for a predefined or user-defined function, and <<arg>> and
- <<actions>> stand for one or more arguments and actions.
- If the condition (<fun> <<arg>>) is true, then the actions in the (then) are
- executed. Else, if the condition is false, the actions in the
- (else) are executed. Actually, the (else) is optional and so you can also
- just write an (if then) rule with no (else) part. Once the (if then
- else) completes execution, CLIPS continues with the next action, if any, on
- the RHS.
- As a simple example of (if then else), enter the following rule. Then
- assert (light green), then (light red) and run.
-
- (defrule traffic-light
- (light ?color)
- =>
- (if (eq ?color green)
- then
- (printout "Go" crlf)
- else
- (printout "light not green" crlf)))
-
- If the light is green, the (then) part will be executed and "Go" will be
- printed out. If the light is not green, the message "light not green"
- will be printed out.
- The (if then else) can be nested. That is, one (if then else) can be
- included within another. The following example shows how one rule can
- handle all three cases of the traffic light. The indentation of the (if)
- parts is to aid readability. Enter and run this for the facts (light
- green), (light red), and (light yellow).
-
- (defrule traffic-light
- (light ?color)
- =>
- (if (eq ?color green)
- then
- (printout "Go" crlf)
- else
- (if (eq ?color yellow)
- then
- (printout "Slow down" crlf)
- else
- (if (eq ?color red)
- then
- (printout "Stop" crlf)
- )
- )
- )
- )
-
- Notice that the closing right parentheses are lined up under their
- corresponding left parentheses. When you have a lot of nesting, it's very
- helpful to someone reading the program to see the parentheses enclosing the
- different parts of the rule. It's also easier on the programmer
- writing the code to tell when all the parentheses are balanced.
- Although the above rule does work, it is shown just as an example of (if
- then else) syntax. The program should really be written as three
- rules to make efficient use of CLIPS. The danger of using an (if then else)
- is that you tend to think in a procedural fashion and so write
- your programs that way. However, CLIPS is not a procedural language. To make
- the best use of CLIPS, you should think in terms of rules and
- not try to structure a CLIPS program in a procedural way.
-
-
- Meanwhile
-
-
- The second type of structure provided by CLIPS is the (while). Although you
- can use (test) and multiple rules to produce a loop, the (while)
- structure can be used to produce a loop within a single rule on the RHS. The
- general form is
-
- (while (<fun> <<arg>>)
- (<<action>>))
-
- where <<action>> stands for one or more actions and <<arg>> are the zero or
- more arguments required by the function, <fun>. The actions of the
- (while) comprise the body of the loop.
- The part of the (while) represented by (<fun> <<arg>>) represents a
- condition that is tested before the actions in the body are executed.
- If the while-condition is true, then the actions in the body will be
- executed. If the while-condition is false, execution continues with the
- next statement after the (while), if any.
- As an example of a while-loop, let's look at the program to generate squares
- of numbers written in a while-loop form. Compare the following
- program with that written in the first section of Chapter 6.
-
- (defrule input-max
- (start-fact)
- =>
- (printout "number of loops? " crlf)
- (bind ?max (read))
- (bind ?count 0) ;initialize ?count to 0
- (while (<= ?count ?max)
- (bind ?square (* ?count ?count))
- (printout "square of " ?count " is " ?square crlf)
- (bind ?count (+ ?count 1))))
-
- The actions in the while-loop body will continue printing out the squares of
- numbers until ?count is greater than ?max because then the while-condition,
- (<= ?count ?max), will be false. Enter and run this new version and you'll
- see it produce the squares of numbers just as the first version
- did.
- Although the (while) version works, it is more inefficient than the example
- in Chapter 6 using multiple rules. CLIPS is really designed for
- efficiency with multiple rules. Trying to make a procedural program out of
- CLIPS negates the advantages of a rule-based language. Carried
- to an extreme, you could write a one-rule program in CLIPS using (while) and
- (if then else). However, this completely defeats the purpose of
- the language for efficiency with multiple rules.
- As another example of the (while) syntax, consider a program to print the
- mean or average of numbers. The following program shows how it
- can be done using a single rule. Enter and run the program to compute the
- average of the five numbers, 1, 2, 3, 4, and 5.
-
- (defrule input-max
- (start-fact)
- =>
- (printout "How many numbers? "crlf)
- (bind ?max (read))
- (bind ?count 1)
- (bind ?sum 0)
- (while (<= ?count ?max)
- (printout "Number " ?count " = ? " crlf)
- (bind ?number (read))
- (bind ?sum (+ ?sum ?number))
- (bind ?count (+ ?count 1)))
- (bind ?answer (/ ?sum ?max))
- (printout crlf crlf "Mean = " ?answer crlf))
-
- Notice that only one rule was necessary and no facts were asserted.
-
-
- Not Meanwhile
-
-
- Although the previous rule for the mean works, it would be convenient if you
- didn't have to count in advance how many numbers there are. That
- is, it would be nice if the program kept count of the numbers that were read
- until a sentinel was found. A sentinel is a special symbol that
- indicates the end of input.
- The following version of the program uses the sentinel "done" to signify end
- of input. Enter and run for numbers 1, 2, 3, 4, and 5. Then
- type "done" when you're asked for the sixth number (do not enter the quotation
- marks). The computer will then print the mean.
-
- (defrule not-mean-while
- (start-fact)
- =>
- (bind ?sum 0)
- (bind ?count 1)
- (printout "Number 1 = ? " crlf)
- (bind ?number (read))
- (while (! (eq ?number done))
- (bind ?count (+ ?count 1))
- (bind ?sum (+ ?sum ?number))
- (printout "Number " ?count " = ? " crlf)
- (bind ?number (read)))
- (bind ?mean (/ ?sum (- ?count 1)))
- (printout crlf crlf "Mean = " ?mean crlf))
-
- The while-loop reads input and assigns it to ?number until a "done" is
- entered. Notice the "not" function, "!", in the while-loop test (!
- (eq ?number done)). This test says to execute the actions in the body of the
- while-loop while ?number is not equal to "done".
- The "eq" function was used instead of the "not equal" operator "!=" because
- "!=" can only be used with numbers, while "eq" can be used with
- both numbers and strings. In fact, you must use "eq" for strings since CLIPS
- has no other function to check the equality of strings.
- An alternative approach to a string sentinel would be to use a special
- number such as 1e38 as sentinel so that you can use "!=". This sentinel
- is picked because it's a number that's unlikely to occur in data. Of course,
- if a number like 1e38 is likely to occur, you should pick another.
- As another simple example of (while), the following rule is very useful if
- you have a lot of (assert) statements to make and don't want to
- use a (deffacts).
-
- (defrule auto-assertions
- (initial-fact)
- =>
- (printout "Assertion ? " crlf)
- (bind ?response (read))
- (printout "Asserted " ?response crlf)
- (while (! (eq ?response done))
- (assert (?response))
- (printout "Assertion ? " crlf)
- (bind ?response (read))
- (printout "Asserted " ?response crlf)))
-
- When you run this rule, it will assert facts until you type "done". You'll
- find it convenient to save utility rules like this and incorporate
- them in programs just as you'd use a subroutine library in other languages.
- As another example of the while statement, consider the program to calculate
- which numbers are perfect squares that was discussed in Chapter
- 8. Instead of requiring a separate rule, make-numbers, to assert the numbers,
- the program can be written as two rules.
-
- (defrule make-numbers
- (initial-fact)
- =>
- (printout "Initial number ? " crlf)
- (bind ?initial (read))
- (printout "Final number ? " crlf crlf)
- (bind ?final (read))
- (while (<= ?initial ?final)
- (assert (number ?initial))
- (printout ?initial crlf)
- (bind ?initial (+ ?initial 1))))
-
- (defrule perfect-squares
- (number ?x)
- (number ?y)
- (number ?z&:(= (* ?z ?z) (+ (* ?x ?x) (* ?y ?y))))
- =>
- (printout "x = " ?x " y = " ?y " z = " ?z crlf))
-
-
- A Rule Of Sorts
-
-
- Suppose you wanted to write a single rule that would sort a list of numbers,
- where some of the numbers might be duplicates. Can it be done?
- The answer is yes as the following program shows. The first rule, called
- input-numbers, allows the user to input the numbers to be sorted
- and asserts them as facts. The second rule, called sort, does all the
- sorting. Actually, the sort rule just prints out the numbers in sorted
- order. However, it would be easy enough to assert the sorted numbers as they
- are printed out and so produce a sorted list of facts.
- Enter the program and run for the numbers 3, -2, 1, 4, 1, and 0. Then type
- "done" (don't type in the double quotes) to stop input and the
- sort rule will print out the sorted numbers.
-
- (defrule input-numbers
- (initial-fact)
- =>
- (bind ?count 1)
- (printout "Number 1 = ? " crlf)
- (bind ?number (read))
- (while (! (eq ?number done))
- (assert (?count ?number))
- (bind ?count (+ ?count 1))
- (printout "Number " ?count " = ? " crlf)
- (bind ?number (read))))
-
- (defrule sort
- ?biggest <- (?count1 ?number1)
- (not (~?count1 ?number2&:(> ?number2 ?number1)))
- =>
- (printout ?number1 crlf)
- (retract ?biggest))
-
- The input-numbers rule puts a different identifying number based on ?count
- in front of each number to be asserted. That way duplicate numbers
- can be entered. For the sample numbers, the asserted facts would be
-
- (1 3)
- (2 -2)
- (3 1)
- (4 4)
- (5 1)
- (6 0)
-
- The "number" field is not neccessary in this example because the program
- uses facts with two fields for storage of the count and number.
- So (initial-fact) can never match because it has only one field.
- Notice how count gives each of the numbers a unique identifier. Otherwise,
- the two number 1's could not be asserted. Of course, if you don't
- have any duplicate numbers, the ?count is not necessary.
- In the sort rule, the first conditional element matches to any number.
- However, the second conditional element is more picky. It says that
- there is no number greater than the number matched by the first conditional
- element. If you think about it, this is just the definition of
- the largest number in a list: there is no greater number.
- Let's look at the second conditional element in more detail to understand
- how it works. The second conditional element requires several things.
- First, the count identifier must be unequal to ?count1. This prevents a fact
- from matching itself. Second, ?number2 must be greater than
- the first number, ?number1, that was matched.
- Suppose both of these requirements are met in the second conditional
- element. Then the logical not in front inverts the result and says it's
- not a match after all. So if you think you've got it tough, imagine what it's
- like to be an inference engine and told that a success is really
- a failure.
- The sort rule finds the biggest number in the fact-list and prints it out.
- Then the (retract) removes the biggest number and the sort rule
- fires again to find the new biggest number. The new biggest is printed out,
- then retracted and the sort rule fires again. This process continues
- until there are no more numbers left in the fact-list.
-
-
- If You Don't Care
-
-
- In the Sort Program, the count numbers were only used as internal identifiers
- of facts. That is, the value of the numbers was not important.
- Only the fact that each count number was unique was important in using the
- counts as identifiers.
- If you don't care what the actual value is, then CLIPS has just the thing
- for you. The gensym function returns a unique symbolic atom every
- time it's called of the general form
-
- genX
-
- where X represents a different number each time gensym is called. The (gensym)
- numbers always start at 1 when CLIPS is first started and increment
- by 1 each time (gensym) is called until you exit CLIPS.
- As an example of (gensym), enter and run the following program to make ten
- calls of (gensym).
-
- (defrule make-gensym
- (initial-fact)
- =>
- (bind ?count 10)
- (while (> ?count 0)
- (bind ?sym (gensym))
- (printout ?sym crlf)
- (bind ?count (- ?count 1))))
-
- You'll see the following output.
-
- gen1
- gen2
- gen3
- gen4
- gen5
- gen6
- gen7
- gen8
- gen9
- gen10
-
- Now run the program again and the symbols will start with gen11.
- You can also start (gensym) off with a value that you specify with the
- setgen function. The (setgen) must be used in a call to an external
- function on the RHS. For example, modify the make-gensym rule as shown below
- and run. Now the (gensym) will start off at 100.
-
- (defrule make-gensym
- (initial-fact)
- =>
- (bind ?count 10)
- (call (setgen 100)) ; add this line
- (while (> ?count 0)
- (bind ?sym (gensym))
- (printout ?sym crlf)
- (bind ?count (- ?count 1))))
-
-
- Now let's apply (gensym) to the sorting program. Instead of using ?count
- and having to do arithmetic to obtain new identifiers, let (gensym)
- do the work. The following program is the (gensym) version. Enter and run it
- for some numbers and you'll see the program sorts just as good
- as before.
-
- (defrule input-numbers
- (initial-fact)
- =>
- (printout "Number ? " crlf)
- (bind ?number (read))
- (while (! (eq ?number done))
- (assert (=(gensym) ?number))
- (printout "Number ? " crlf)
- (bind ?number (read))))
-
- (defrule sort
- ?biggest <- (?count1 ?number1)
- (not (~?count1 ?number2&:(> ?number2 ?number1)))
- =>
- (printout ?number1 crlf)
- (retract ?biggest))
-
- The only difference between the two sorting programs is that the (gensym)
- version does not print out identifying numbers as you input the
- numbers.
-
-
- Whiles In Whiles
-
-
- As long as we're enhancing the program, let's go all the way and have it ask if
- we want to do another. Although this could be done by having
- another rule triggered when the not-mean-while rule ends, let's be elegant and
- do everything within one rule. The following program shows
- nested while-loops. One loop, called the inner loop, is entirely contained
- within another, the outer loop. Run for an input of 1, 2, 3, 4,
- and 5. Type "done" and you'll see the result mean. Then type "n" and the
- program will ask for a new set of numbers for a mean calculation.
-
- (defrule stop-not-mean-while
- (start-fact)
- =>
- (bind ?response n)
- (while (eq ?response n)
- (bind ?sum 0)
- (bind ?count 1)
- (printout "Number 1 = ? " crlf)
- (bind ?number (read))
- (while (! (eq ?number done))
- (bind ?count (+ ?count 1))
- (bind ?sum (+ ?sum ?number))
- (printout "Number " ?count " = ? " crlf)
- (bind ?number (read)))
- (bind ?mean (/ ?sum (- ?count 1)))
- (printout crlf crlf "Mean = " ?mean crlf)
- (printout "Stop program ? (y or n) " crlf)
- (bind ?response (read))))
-
- The inner loop will keep asking for input data until the user inputs "done".
- Then the program will print out the mean and ask if another
- is to be done. If the user answers "n", the program ends since the outer
- while-loop test, (eq ?response n), is true.
- Although the if and while are very powerful, they should be used sparingly.
- If you're using them a lot, your program is becoming sequential
- and an expert system language may not really be the best choice for your
- application.
-
-
- The Missing Evidence
-
-
- Sometimes the absence of an item reveals more information than it's presence,
- especially in murder mysteries. CLIPS allows you to specify the
- absence of a fact as a conditional element on the LHS using the logical not
- function.
- The syntax of the conditional element with (not) is
-
- (not (pattern))
-
- where "pattern" represents the fact that is to be matched.
- As a simple example, think back to the problem of telling a robot when to
- cross the street. The rules which tell when to walk are the following.
-
- If there is a walk-sign that says walk
- then walk
-
- If there is no walk-sign and the light is green
- then walk
-
- The (not) function can be conveniently applied to the simple rules above as
- follows.
-
- (defrule walk-sign-walk
- (walk-sign walk)
- =>
- (printout "Walk" crlf))
-
- (defrule no-walk-sign
- (light green)
- (not (walk-sign ?))
- =>
- (printout "Walk" crlf))
-
- Notice that a wildcard question mark is used for matching the second atom of
- (walk-sign) in the no-walk-sign rule. The rule won't be triggered
- on just (walk-sign) since we assume the second atom contains the status of the
- sign. That is, if there is a (walk-sign), it will be either
- (walk-sign walk) or (walk-sign dont walk).
- Enter these rules and assert (light green). Check the agenda and you'll see
- that only no-walk-sign will be triggered. Now assert (walk-sign
- walk) and you'll see that no-walk-sign will be removed from the agenda and
- walk-sign-walk will be put on the agenda.
- It's important to realize that the no-walk-sign rule is not the same as a
- rule with one conditional element like
-
- defrule green-light
- (light green)
- =>
- (printout "walk" crlf))
-
- The green-light rule will be triggered whether there is or is not a
- (walk-sign) fact. However, the no-walk-sign rule will be triggered only
- if there is not a (walk-sign). The difference between the rules is subtle but
- important.
- A (not) can only be used to negate one conditional element at a time. You
- must use a separate (not) for each conditional element to be negated.
- Problems
-
-
- 1. Rewrite problems 1 - 3 of Chapter 6 using a (while) statement to do all the
- calculations in one rule.
-
-
- 2. Write a program with one rule using nested (while) loops to generate the
- multiplication tables from 1 to 9. The output of the program should
- be in the form
-
- 1 x 1 = 1
- 1 x 2 = 2
- 1 x 3 = 3
-
- and so forth.
-
-
- 3. Write a program to control the operation of a portable electric generator
- driven by a Diesel engine. A simplified diagram is shown in Fig.
- 10-1. The generator produces electricity if the engine is working properly.
- The engine is working properly if there is a normal amount of
- oil for lubrication, fuel, and water for cooling.
- Some of the possible abnormal conditions and corrective actions are shown in
- the following table. Based on the diagram, add other abnormal
- conditions and corrective actions to the table. Each action should also print
- out a message of the actions taken and the conditions that triggered
- the action.
-
- Conditions Actions
-
- Oil pressure is low : Check Reserve Tank ORT-1
- > 50 and <= 100
-
- Oil pressure too low : Switch on Backup Oil Pump OP-2
- <= 50 Check Oil Pump OP-1
-
- Water temperature high : Check Water Pump WP-1
- > 125 and <= 165
-
- Water temperature too high: Turn on WP-2
- > 165
-
- Electric demand is high : Increase RPM to 2000
- > 900 and <= 1000
-
- Electric demand very high : Increase RPM to 2500
- > 1000 and < 1100
-
- Write a program that asks the user to input abnormal conditions. That is,
- the user can input that the oil pressure is low or that electric
- demand is very high. The program will then print out the corrective action
- and then ask the user for new conditions. Also, include rules for
- excessive conditions such as no fuel that will cause a shutdown.
- In order to simplify input of abnormal conditions by the user, display all
- of the possible conditions in the form of a menu. For example,
- part of the menu might be
-
- Oil pressure low-------1 Water temperature high-------3
- Oil pressure too low---2 Water temperature too high---4
- Exit program----------99
-
- The user would input the menu numbers of conditions.
- Assume that a reading never just jumps to a too high or too low condition.
- That is, the condition will first pass through a low or high before
- becoming too low or too high. Thus all of the appropriate actions for the low
- or high will be taken before the too low or too high conditions.
- Some useful information is the following.
-
- Fuel low : 125
- Fuel too low : 50
-
- The engine will not operate if it is out of fuel, oil, or water.
-
- If any one backup system is operating, the system cannot meet an electric
- demand that is very high and so a brownout will occur.
- Print out a brownout warning message.
-
- If more than one backup system is operating, and electric demand is very high,
- a blackout will occur. Print out a blackout warning message.
-
- If the electric demand is greater than or equal to 1100, a brownout will occur
- because the RPM cannot be increased beyond 2500. Print out a
- brownout warning message.
-
-
-