CLIPS Tutorial 5 - Truth and control

This tutorial uses the following data. If you're reading this on screen, you can cut it and paste it into the CLIPS editor. If you're reading it on paper, you can still do this but it will make a mess of your monitor.

(deftemplate personal-data
	(slot name)
	(slot age)
	(slot weight)
	(slot smoker)
	(multislot date_of_birth)
)

(deffacts people
	(personal-data (name adam) (weight 60) (age 30) 
		(smoker no) (date-of-birth 18 06 1970))
	(personal-data (name brenda) (weight 120) (age 45) 
		(smoker yes) (date-of-birth 18 06 1955))
	(personal-data (name charles) (weight 120) (age 60)
		(smoker yes)(date-of-birth 18 06 1940))
)

(deffacts data
	(date 18 06 2000)
)

In the last tutorial, we learned about several conditional elements: and, or, not, test, forall and exists. There is still one more: logical. The logical conditional element assists with the perennial problem of truth maintenance. Enter and run the following rule, using the facts defined above.

(defrule cardiac-risk
	(person (name ?name) (smoker yes) (weight ?weight))
	(test (> ?weight 100))
=>
	(assert (cardiac-risk ?name))
)

This rule tests whether a person is overweight and a smoker. If he is, then it asserts a warning that his heart is at risk. Now what happens if the person gives up smoking, or loses weight? The warning fact will still be present, because it is not linked in any way to the rule which created it. This means that the fact base is no longer consistent with reality. We can overcome the problem by using the logical element. Modify the rule you have just entered so that it looks like this:

(defrule cardiac-risk
	(logical (person (name ?name) (smoker yes) (weight ?weight)))
	(logical (test (> ?weight 100)))
=>
	(assert (cardiac-risk ?name))
)

When we run this rule, the results are identical to those generated by the first version (i.e. the facts (cardiac-risk brenda) and (cardiac-risk charles) are asserted). The difference occurs if we change the initial data. Make sure the fact window is open, then having run the rule, locate the fact-index of Brenda's personal data (on my system, it's 2) and type at the command line

CLIPS> (modify 2 (weight 80))

As if by magic, the fact (cardiac-risk brenda) disappears from the fact list. By using the logical keyword, we have created a link between the fact asserted and the premises on which it was based. When the premises changed, their results were no longer valid, so they were removed. So why not make everything logical in this way? Firstly, it increases the memory and processing time needed - in a complex system, there can be many links to check. Secondly, it's not always appropriate - for most applications, the standard facts are quite sufficient.

As you are now aware, the order in which rules are triggered in CLIPS is not easily controlled. Any rule which matches a fact may be placed on the agenda in any position. How, then, are we to establish any sort of control over our expert systems? There are two primary solutions to this problem - by using control facts or salience. We'll come back to salience later. A control fact is one whose only purpose is to direct program operation rather than to express knowledge about the problem domain. By including these control facts in the left hand sides of rules, we can control when the rules will fire. For example, suppose we wish to check all our personal data records to see whose birthday it is today, and update their ages accordingly. If we have a fact of the form (date 18 6 2000) in the fact list representing today's date, then the rule

(defrule birthdays-today
	?person <- (personal-data (age ?age) (date-of-birth ?day ?month ?))
	(date ?day ?month ?)
=>
	(modify ?person (age (+ ?age 1)))
)

might seem a reasonable way of doing this. Try it, and see what happens. You might want to know that pressing ctrl-break will stop a CLIPS program. Why doesn't this work? The reason it gets into an infinite loop is that the modify function actually retracts the personal-data fact and asserts a new one with the updated data in it. This new fact then matches the same rule, which causes it to be modified, and so on until the end of time. The way around this problem is to divide the task into discrete phases and use control facts to execute them in order. there are really two phases to this problem - identifying all the people who have birthdays today, and then updating their ages. We can implement this using the four rules below:

(defrule birthdays-today
	(check-birthdays)
	(personal-data (name ?name) (date-of-birth ?day ?month ?))
	(date ?day ?month ?)
=>
	(assert (birthday ?name))
)

(defrule done-checking-birthdays
	?check-birthday-fact <- (check-birthdays)
	(forall (and (personal-data (name ?name) (date-of-birth ?day ?month ?))
		     (date ?day ?month ?)
		)
		(birthday ?name)
	)
=>
	(retract ?check-birthday-fact)
	(assert (update-ages))
)

(defrule update-ages
	?person<-(personal-data (name ?name) (age ?age) (date-of-birth ?day ?month ?year))
	?birthday-fact<-(birthday ?name)
	(update-ages)
=>
	(modify ?person (age (+ ?age 1)))
	(retract ?birthday-fact)
)

(defrule done-updating-ages
	?update-age-fact<-(update-ages)
	(not (birthday ?))
=>
	(retract ?update-age-fact)
)

The phases are controlled by two facts: (check-birthdays) and (update-ages). In phase 1, which is triggered by the appearance of the fact (check-birthdays), rule birthdays-today asserts a new fact for each person who has a birthday today. Rule done-checking-birthdays detects when this process is finished, retracts (check-birthdays) and asserts (update-ages). The second phase now begins, and rule update-ages modifies each person in turn, retracting the birthday facts in turn as it does so. When there are no more birthday facts, rule done-updating-ages fires and retracts (update-ages) for good housekeeping purposes. This may all seem a little complex for what should be a simple task, and indeed it is. But it does illustrate the use of control facts.

The other way of controlling rule firing is by using the salience property of rules. When a rule is declared, it may be given a salience value between -10000 and 10000, the default value being 0. All other things being equal, rules with a higher salience are fired before rules with a lower salience. Consider the following two rules:

(defrule poke-fun-at-smokers
	(personal-data (name ?name) (smoker yes))
=>
	(printout t ?name " is a fool." crlf)
)

(defrule worry-about-thin-people
	(personal-data (name ?name) (weight ?weight))
	(test (< ?weight 80))
=>
	(printout t ?name " is looking a bit thin." crlf)
)

As things stand, if you run those rules then they will both fire, but the order of their firing will depend only upon the order in which the facts on which they depend were created. If, however, you modify them thus:

(defrule poke-fun-at-smokers
	(declare (salience 10)) 
	(personal-data (name ?name) (smoker yes))
=>
	(printout t ?name " is a fool." crlf)
)

(defrule worry-about-thin-people
	(declare (salience 20)) 
	(personal-data (name ?name) (weight ?weight))
	(test (< ?weight 80))
=>
	(printout t ?name " is looking a bit thin." crlf)
)

then the higher salience of rule worry-about-thin-people will ensure that it fires first. As a general rule, try to use salience sparingly. If you find you need many levels of salience (more than four is a good rule of thumb) then you should probably consider either (a) writing your program in a language which gives you the level of control you desire, or (b) rewriting your program to suit the production system paradigm. There is a third way of controlling the execution of rules or groups of rules, and that is by collecting them into modules, only one of which is active at any given time.