0. Introduction
Over the last year and a half or so, I've been learning about the CLIPS programming language. A team of developers at NASA created CLIPS in the mid-'80s. It was released into the public domain in the mid-'90s, and it has been actively developed by one of the original team members Gary Riley ever since.
CLIPS has this air of mystery about it. There's no npm-like service that centralizes common CLIPS "libraries." A lot of writing about the language appears, on the surface, academic or higher-level. To top it off, the Rete algorithm central to CLIPS can be rather intimidating to wrap one's head around.
Don't let these things deter you; CLIPS is well worth learning. Its Rules-based approach is a good primer to learning neural networks and AI. With this tutorial, you'll take your first steps into Rules Engines and Expert Systems.
First, we'll talk about why you should consider using CLIPS. At the heart of CLIPS is the Rete algorithm. This algorithm does a few very nice things for us. For one, it determines the order in which Rules should run. This allows us to focus on what the domain business logic is of our program rather than how the software runs. Second, the Rete network "caches" common calculations our software will make. Those who have written an in-app cache understand the benefits of this feature.
Another huge win for CLIPS is the excellent documentation and support provided by the lead developer Gary Riley. Take a look at the various guides of all levels linked in the documentation section of the CLIPS homepage. Also, Mr. Riley is very active in various forums providing support for CLIPS. Once again, look at the main CLIPS website to find links to the various avenues of support.
Finally, CLIPS is still actively developed; its latest release 6.4.2 was published in January of 2025. Pretty incredible!
0.a This Tutorial
This tutorial is greatly inspired by the fantastic
A Tour of
Go. Run the examples presented in this tutorial by clicking the
(batch*) button under the code. The code in the box is
editable, so tweak the code to test your curiosity. The output from
the code will display in the box below the buttons. Navigate between
the previous and next Chapters by clicking the links at the bottom of
the screen. The left link (←) will navigate you to the previous
chapter while the right link (→) will advance to the next chapter.
Between these two links will show the name of the current Chapter.
Click that link to reveal a list of all Chapters in this book.
Clicking one of these Chapters should navigate you to the appropriate
Chapter.
; Welcome to the tutorial! This is known as a "comment." ; It is meant to help developers ; understand a particular line of code better. ; ; Click the (batch*) button to run this code sample. ; You will see CLIPS code written in this area of the webpage ; throughout this tour. ; ; This is a WASM compilation of CLIPS run completely in your browser; ; no server involved! (println "Hello, world")
- 0. Introduction
- 1. Saying Hello →
1. Saying Hello
The classic "first program" is the "Hello, World" program. This will
show you how to output some text from your program. Click the
(batch*) button under the code on the right hand side.
You should see text appear in the box beneath the buttons.
With CLIPS, there are a few ways we can print text to the screen.
Using (print) will print words out, while
(println) will print words out with a new line at the
end. In the code sample to the right, you can see us using
(print) to print the word "Hello". We then use
(println) to print ", World!" and a new line character.
Things printed after this text will appear on the next line.
(printout t) is like (print) and
(println), but it allows you to send text to a given
"router." In this case, we send the text to the router t,
an alias for stdout. This is the router that
(print) and (println) send text to by
default. We also use the text crlf as the last argument
to (printout. We do this so that our text "breaks" at the
end of the line; the next thing we print will appear beneath this
current line. crlf stands for "carriage return line
feed," which means "move the cursor to the beginning of the next
line."
(format t) allows you to print a sentence structure that
contains "format flags." The %s and %n are
format flags that specify a string and newline respectively. The next
argument (in this case "fun") will be substituted in place of the
%s. %n will be replaced with a new line
character. To see more of these "format flags," check out the
Basic
Programming Guide -- specifically the section entitled 12.4.6
Formatted Printing -- to learn more.
(print Hello) (println ", World!") (printout t "Welcome to this tour of CLIPS" crlf) (format t "I hope that you have %s!%n" fun)
- ← 0. Introduction
- 1. Saying Hello
- 2. Learning the Rules →
2. Learning the Rules
Rules are an important part of understanding how CLIPS works. Rules consist of an "antecedent" and a "consequent." The antecedent is also known as the "if portion" or the "left-hand side." This part consists of the conditions that must be satisfied for the Rule to run.
The consequent is also known as the "then portion" or the "right-hand side." This part describes what will happen when the Rule is run.
These two pieces are separated by a =>. The antecedent
is before this symbol while the consequent is after it.
In this example, we define a Rule called
will-run-no-matter-what. This particular Rule has nothing
on the left-hand side so it will always run. The right-hand side
specifies that a (println) will run.
Finally after the Rule is defined, there is a (run). In
order for this to work, click the (batch*) button.
(defrule will-run-no-matter-what
=>
(println "Hello from a rule, world!"))
(run)
- ← 1. Saying Hello
- 2. Learning the Rules
- 3. Just the Facts →
3. Just the Facts
In addition to Rules in CLIPS, we have "Facts." A Fact represents
"data" in CLIPS. In order to put data into CLIPS, we
(assert). Once we have (assert)ed Facts,
they are said to be "in working memory."
In the code to the right, we (assert) one
(name Fact with ryjo in its second field.
We then print a single line of text with our old friend
(println).
(facts) is a new one. We use this to print out the Facts
stored in Working Memory. In this case, we see 1 fact printed with an
index 1 as denoted by f-1.
Finally, we'll (retract this Fact that we just asserted.
We can reference this Fact with its index 1.
Note: If you click (batch*) multiple times in a row, an
error will appear. This is because we must (reset) or
(clear) the environment to reset the ids assigned to
Facts in Working Memory. If we do not, the Fact that we assert will
have an id of 2, not 1. If you click
(reset) or (clear) and then click
(batch*) again, the code will behave as expected.
Bonus: if you are returning to this Chapter running the code in
the Chapter "Negative, Ghostrider",
you may see a few more than 1 Fact returned. That's because we do not
(clear) at the top of the code to first "clean up" the
environment. You'll learn more about (clear) in Chapter
"Clearly Resetting".
(assert (name ryjo)) (println "The current facts: ") (facts) (retract 1) (println "After retracting: ") (facts)
- ← 2. Learning the Rules
- 3. Just the Facts
- 4. Running the Engine →
4. Running the Engine
The "Facts" stored in "Working Memory" can be used to satisfy the LHS (left-hand side) of a rule. Remember: when the LHS of a Rule is satisfied, the Rule's RHS (right-hand side) will run.
In the code to the right, we create a Rule using
(defrule) called greet. This Rule will
"activate" when a Fact that matches the form (name ?) is
entered into Working Memory. Once we (run) the rules
engine, the Right Hand side of this Rule will run if a
name Fact exists in the system with a second field.
Next, we (assert) a name Fact with the value
ryjo in its second field. Once we do this, the
greet rule is "activated;" when we (run) our
Rules Engine next, this Rule's RHS will be executed.
One thing to note: we can assert facts and define rules in whichever order we want; we could assert the fact before defining the rule, and the rule would still activate. Give it a try!
Bonus: if you are returning to this Chapter running the code in
the Chapter "Negative, Ghostrider",
you may see a few more than 1 Fact returned. That's because we do not
(clear) at the top of the code to first "clean up" the
environment. You'll learn more about (clear) in Chapter
"Clearly Resetting".
(defrule greet
(name ?name)
=>
(println "Hello " ?name "!"))
; Try moving this next line above the defrule
(assert (name ryjo))
(run)
- ← 3. Just the Facts
- 4. Running the Engine
- 5. Negative, Ghostrider →
5. Negative, Ghostrider
Sometimes you want to activate a Rule when there is a Fact in Working
Memory that does not match a certain pattern. The tilde
(~) character is used to negate a given pattern. It's
like saying "a Fact that does not have this here."
Note that we did not capture the name in order to print it out in the RHS of this Rule like we did in Chapter "Running the Engine". We'll cover how to use these names in Chapter "Capturing the Name".
First, we (clear) our environment of all previously
defined Rules and Facts from the CLIPS Environment. We then
(assert two (names: Zach and
Zethus. Finally, our Rule non-ryjos-only
will "activate" when a Fact matching the pattern (name
~ryjo) enters working memory. This pattern might be read in
English as "a Fact that starts with name and does not
have ryjo in its second field." Finally, the RHS of this
Rule prints the text "Hello, not ryjo!"
Bonus: After you (batch*) this code, try
navigating to Chapter "Running the
Engine"; you'll see that the previously defined greet
Rule runs for these two new names. That's because the examples in this
tutorial use the same CLIPS Environment. In this example, we use
(clear) at the top which removes everything defined in
previous examples. You will see this convention more in following
chapters.
(clear) (assert (name Zach) (name Zethus)) (defrule non-ryjos-only (name ~ryjo) => (println "Hello, not ryjo!")) (run)
- ← 4. Running the Engine
- 5. Negative, Ghostrider
- 6. Capturing the Name →
6. Capturing the Name
Let's combine two previously presented concepts; we'll both capture
the value as described in Chapter
"Running the Engine" and defining a
pattern the value adhere to as presented in Chapter
"Negative, Ghostrider". We'll do
this by placing and & between the variable used to
capture the name ?n and our pattern ~ryjo.
The & symbol is known as a "Connective Constraint" and it
means "and." We can read the pattern (name ?n&~ryjo) as
"a value and it is not ryjo."
Additionally, we (assert) three facts. Only two of them
will "activate" the non-ryjos-only rule we define right
below it. Something neat to make note of: we only need to call
(assert) once in order to put three new Facts into
Working Memory.
We define a second rule ryjo here to demonstrate the
"opposite" of the non-ryjos-only rule. This will activate
when a Fact enters Working Memory that looks exactly like (name
ryjo).
Note how the Rules in this example don't execute in the order in which
they were written. The reason for this is the order in which Rules
are run is determined algorithmically. Read more about how CLIPS
determines the order in which Rules are run in section 5.2 Basic
Cycle Of Rule Execution of the
Basic
Programming Guide. We can use (salience to explicitly
declare the order in which Rules run. We'll discuss this in Chapter
"Prominently Salient".
(clear) (assert (name Gabe) (name ryjo) (name John)) (defrule non-ryjos-only (name ?n&~ryjo) => (println "Hello, " ?n "!")) (defrule ryjo (name ryjo) => (println "Oh hey there")) (run)
- ← 5. Negative, Ghostrider
- 6. Capturing the Name
- 7. And/Or →
7. And/Or
We can make use of "Connective Constraints" to better refine the LHS
of our Rules. For example, in the Rule ors in the code
editor, we match on a Fact whose second field matches either
Gabe or Peter. This can be read as "A
name Fact whose first field we store the variable
?name and matches either Gabe or
Peter."
In the and-nots Rule, we store the second field of a
name Fact in the variable ?name. This time,
we say "the value in the first field cannot match ryjo
and it cannot match Zach and it cannot match
Zethus."
(clear) (assert (name Gabe) (name ryjo) (name John) (name Zach) (name Zethus) (name Peter)) (defrule ors (name ?name&Gabe|Peter) => (println "Greetings, " ?name)) (defrule and-nots (name ?name&~ryjo&~Zach&~Zethus) => (println "Hi there, " ?name)) (run)
8. Predicate Constraints
Predicate Constraints sound fancy, but they're pretty straight
forward. Basically they allow you to further specify what value a
field holds. For example, with our current knowledge, we can specify
exact matches for values such as (name ?n&ryjo).
Predicate Conditions allow us to perform a function on the value to
determine if it matches.
In our code example for this chapter, we write a basic Rule that describes a rule that allows entry into a bar. In the United States of America, we require a person to be 21 years of age or older. If they are under 21 years of age, they are not allowed into a bar.
Note the familiar form of ?variable&. We use the
and symbol here (&) to signify that
?variable must match certain criteria. This allows us to
read this as "the age of a person and it is greater than or equal to
21" in the allow-entry-to-bar Rule.
Also note that we make use of 2 different Facts in Working Memory to
activate our rules: the name and age Facts.
Also note that in the LHS of our Rules, we specify that the first
field of these Facts must match by using ?person in both.
Since ?person is used in both of these conditions, we say
that this Rule is "activated" when these Facts have the same value in
their first field.
For example, the (name ryjo) Fact and the (age ryjo
32) Fact will activate the allow-entry-to-bar Rule
together. Both the value in the first field match for these Facts
and the number 32 is greater than or equal to 21. The Facts
(name ryjo) and (age someone 20) will not
activate the no-entry-to-bar Rule. The number
20 is indeed less than 21, but these two Facts will not
activate the Rule together. However, the 2 Facts (name
someone) and (age someone 20) will. Not only is
20 less than 21 as noted in the Predicate
Condition (>= ?a 21), but the Symbol someone
matches in both Facts.
(clear) (defrule allow-entry-to-bar (name ?person) (age ?person ?a&:(>= ?a 21)) => (println ?person " is over 21 years old and can enter the bar!")) (defrule no-entry-to-bar (name ?person) (age ?person ?a&:(< ?a 21)) => (println ?person " is not allowed in (under 21).")) (assert (name ryjo) (age ryjo 32) (name someone) (age someone 20)) (run)
- ← 7. And/Or
- 8. Predicate Constraints
- 9. Test Conditional →
9. Test Conditional
This example looks like the previous section. Instead of using
&:, we define our constraints using the
(test Conditional Element.
The output for this example should look very similar to last example's.
(clear) (defrule allow-entry-to-bar (name ?person) (age ?person ?a) (test (>= ?a 21)) => (println ?person " is over 21 years old and can enter the bar!")) (defrule no-entry-to-bar (name ?person) (age ?person ?a) (test (< ?a 21)) => (println ?person " is not allowed in (under 21).")) (assert (name ryjo) (age ryjo 32) (name someone) (age someone 20)) (run)
- ← 8. Predicate Constraints
- 9. Test Conditional
- 10. One forall →
10. One forall
Sometimes you want to know if all Facts in Working Memory match a
given pattern. (forall provides this check. In our
example, (forall will match if every (name
Fact in working memory can be matched with an (age Fact
(and vice versa).
In our example, the Fact (name another) does not have an
(age Fact with a matching value ?n.
Therefore, our Engine tells us that the "Roster has been declined."
(clear) (assert (name ryjo) (age ryjo 32) (name someone) (age someone 20) (name another)) (defrule accept-roster (forall (name ?n) (age ?n ?)) => (println "Roster has been accepted")) (defrule decline-roster (not (forall (name ?n) (age ?n ?))) => (println "Roster has been declined")) (run)
- ← 9. Test Conditional
- 10. One forall
- 11. Aliens exists →
11. Aliens exists
This is a bit of a longer example, but it showcases a few things
combined together. (exists will match once if a Fact in
Working Memory matches the pattern within. It will not run again while
the condition continues to match. In our example, we match for "any
fact that starts with (age and is followed with two
values." When we use ?, it doesn't matter what these
values are; it only matters that they are there at all.
Try modifying our any-age-reported Rule to capture the
?name and (println ?name) it in the RHS of
the Rule.
We also use (forall to determine when every user has
reported their age.
It may seem strange to give up control of our code's order of execution. Remember, this lets us focus on our Business Logic rather than Control Flow of our domain. Make sure to review the section Performance Considerations in the Basic Programming Guide to learn how to write code in harmony with this approach.
(clear)
(defrule any-age-reported
(exists (age ? ?))
=>
(println "Users have begun to report their age"))
(defrule no-ages-reported
(not (age ? ?))
=>
(println "No user has reported their age yet..."))
(defrule reported-age
(name ?n)
(age ?n ?a)
=>
(format t "%s has reported their age: %d%n" ?n ?a))
(defrule has-not-reported-age
(name ?n)
(not (age ?n ?))
=>
(println ?n " has not yet reported their age"))
(defrule all-ages-reported
(forall (name ?n) (age ?n ?))
=>
(println "Everyone in the system has reported their age"))
(assert
(name ryjo)
(name someone)
(name another))
(println "First run results:")
(println "------------------")
(run)
(println " ")
(assert (age ryjo 32))
(println "Second run results:")
(println "-------------------")
(run)
(println " ")
(assert (age someone 20)
(age another 52))
(println "Third run results:")
(println "------------------")
(run)
- ← 10. One forall
- 11. Aliens exists
- 12. Do you read(line) me? →
12. Do you read(line) me?
Sometime you want to take input from the user rather than "hard-code"
their name, age, and other details. We can take input from the user
using (readline).
In this example, we take input from the user, then we tell them what they entered. Note: when you run this example, a prompt will pop up on your screen. Enter your text into the field, then click the "OK" button.
Similarly, (read) will take the input from a user, but
it'll only take a single word. Change (readline) to
(read) in the code editor. If you enter "foo bar 123"
into the text box and then click the "OK" button, it'll only echo out
"foo."
(clear) ; replace (readline) with (read) and note the differences (println "You entered: " (readline)) (run)
- ← 11. Aliens exists
- 12. Do you read(line) me?
- 13. Say the secret word →
13. Say the secret word
Be warned: note that there is a line commented out in this
example. If you uncomment this line, the pop-up prompt will continue
to show until you enter the word secret.
The reason that this loop occurs is because we (retract
the (word Fact which we captured as ?f in
the LHS of the user-did-not-say-secret Rule. This
triggers the pattern (not (word ?)) in the
get-word-from-user which is the absence of a
(word Fact with one field in Working Memory.
This example is a little more complex, but it provides a good example of a way in which you can "validate" user input. This is not the only way, mind you. It is, however, a good example of how the "main loop" of a program can be made using only Rules and Facts.
(clear) (defrule get-word-from-user ;uncommenting this line will cause a prompt to popup ;until you enter "secret" in the textbox ;) ;(not (word ?)) => (print "Enter your word: ") (bind ?word (read)) (println ?word) (assert (word ?word))) (defrule user-said-secret (word secret) => (println "You said the secret word! Great job")) (defrule user-did-not-say-secret ?f <- (word ~secret) => (retract ?f) (println "Try again...")) (run)
- ← 12. Do you read(line) me?
- 13. Say the secret word
- 14. So many words →
14. So many words
The (readline) function will capture user input and
return a string. If the user's input has any spaces between words, we
can use (explode$ to break the input up into a
"multifield value." This lets us define patterns in our Rules that
will match on individual words entered by the user.
Take a look at the Rule first-word-one. This Rule will
activate when the first word entered by the user is "one."
Similarly, the Rule last-word-last will activate when the
last word entered is "last."
The anywhere Rule will trigger if "anywhere" is
entered anywhere in the prompt. The $? will match for any
number of words. For example, we see in the counter Rule
the pattern (exploded-words $?words). This means "match
any number of values and reference them as a "multifield value"
?words. Just like ?, $? allows
us to say "match on any number of fields" without referencing it in
the RHS.
Speaking of the counter Rule: take a look at
(length$ ?words). As you may have guessed,
(length$ will return the number of values in the
multifield value passed to it. In this case, it returns the number of
words (as referenced by ?words) entered by the user in
the prompt.
(clear) (defrule get-words-from-user => (assert (words (readline)))) (defrule catcher (words ?words) => (println "You said: " ?words) (assert (exploded-words (explode$ ?words)))) (defrule counter (exploded-words $?words) => (println "That's " (length$ ?words) " words total.")) (defrule silence (exploded-words) => (println "Ah, the strong silent type...")) (defrule first-word-one (exploded-words one $?words) => (println "Your first word was 'one'")) (defrule last-word-last (exploded-words $?words last) => (println "Your last word was 'last'")) (defrule anywhere (exploded-words $? anywhere $?) => (println "At some point, you typed 'anywhere'")) (run)
- ← 13. Say the secret word
- 14. So many words
- 15. Prominently Salient →
15. Prominently Salient
By using (declare (salience in our Rule, we can specify
the "priority" of a Rule. This allows us to declare that some Rules
must always run before others. We "hard-code" the execution order
for our rules, and the Rete algorithm builds around it.
Rules have a salience of 0 by default. Rules with a
salience greater than 0 will execute before them.
Similarly, Rules with a lower salience will execute after them.
According to the
Basic
Programming Guide, this feature should be used sparingly:
Despite the large number of possible values, with good design there's rarely a need for more than five salience values in a simple program and ten salience values in a complex program.
For the most part, a Rules Engine should be order independent. This allows CLIPS to do the "heavy lifting" of determining when Rules run. This is subtle, but rather powerful. With Rules, you do not "pass" values into a "function" that runs and returns a value. Instead, the algorithm "activates" Rules based on "matched" Facts in Working Memory. This decouples the data stored in your system (Facts) from the business logic (Rules).
This decoupling can help express the domain you're modeling with less references to the "control flow" of the software. The more transparent the software, the closer your system brings the user to the "truth" of your domain.
(clear)
(defrule third
(declare (salience 0))
?f <- (my-fact)
=>
(println "Last!")
(retract ?f))
(defrule never-runs
(declare (salience -1))
(my-fact)
=>
(println ":("))
(defrule first
(declare (salience 2))
(my-fact)
=>
(println "First!"))
(defrule second
(declare (salience 1))
(my-fact)
=>
(println "Second!"))
(assert (my-fact))
(run)
- ← 14. So many words
- 15. Prominently Salient
- 16. What's your function? →
16. What's your function?
Functions allow you to reference chunks of code that you write repeatedly throughout your program. If we were writing a book, we may find it easier to "wrap" text that has been spoken in quotes as well as a reference to the speaker. We do the same for shouting.
We can also store difficult-to-remember formulas as Functions.
Personally, I have trouble remembering the formula for converting
fahrenheit to celsius. With the help of deffunction, I
can store this calculation and reference it easily.
Take note of the (*, (- and (/
functions. These are basic math operations: multiplication,
subtraction and division. In CLIPS, we write the math problem 1
+ 2 as (+ 1 2). That's because CLIPS uses a
"LISP-like" syntax.
(clear) (deffunction say (?words) (println "You say, \"" ?words ".\"")) (say "Hello, world") (deffunction shout (?words) (println "\"" ?words "!\" you shout.")) (shout "Hello, world")' (deffunction fahrenheit-to-celsius (?f) (* (- ?f 32) (/ 5 9) )) (println "50°F is " (fahrenheit-to-celsius 50) "°C")
- ← 15. Prominently Salient
- 16. What's your function?
- 17. Templatize me →
17. Templatize me
Templates are used to define a formal structure for Facts. By using
(deftemplate person in our example, we describe the
structure of a (person Fact. We wrote Facts as lists of
values in previous Chapters. We would describe these Facts as having
ordered "fields." (person Sally "Hi") would describe a
(person Fact with the values Sally in its
second field and "Hi" in its third field.
With Templates, we are no longer bound to storing people's information
in a particular order. Instead, we store values in named "Slots." For
example, we can choose to define our Fact as (person (greeting
"Hi") (name Sally)), greeting first. We can also define
"default" values for these named slots and omit them in asserted
Facts.
Using Templates gives us some flexibility in how we define our Facts
as well as how we write the matching patterns in the LHS of our Rules.
Take a look at the Rule people-greet-each-other. Our
second pattern matches "a person whos name is not
?name1." We do not need to include a reference to this
Fact's (greeting Slot in this Rule, so we can omit it.
You'll also notice we define a default value for
(greeting in the Template's definition. There are many
other ways to describe a Slot besides default. Take a
look at the
Basic
Programming Guide in the section titled "Deftemplate Construct" to
learn what these are.
(clear) (deftemplate person (slot name) (slot greeting (default Hello))) (assert (person (name Greg)) (person (name Sally) (greeting Hi)) (person (name Sam) (greeting Howdy))) (deffunction greet (?name1 ?name2 ?greeting) (println ?name1 " says \"" ?greeting "\" to " ?name2 ".")) (defrule people-greet-each-other (person (name ?name1) (greeting ?greeting1)) (person (name ?name2&~?name1)) => (greet ?name1 ?name2 ?greeting1)) (run)
- ← 16. What's your function?
- 17. Templatize me
- 18. Clearly resetting →
18. Clearly resetting
Let's talk about (clear) and (reset). We're
familiar with (clear) which makes our environment like
new again. No more Rules, no more Facts.
(reset) does a little less. It retracts all Facts in your
system and Rule activations while leaving the Rules themselves
defined. In this way, you can use (reset) to return your
Engine to a known default "state" before running it again with new
input.
What happens if we need some Facts asserted to return to the "default"
state? Introducing (deffacts. When you use
(reset), the Facts defined in your (deffacts
will be asserted. Consider the example in this chapter and its output.
On the first run, we (assert 2 more fruit Facts than in
the second. As a result, we see two more people eat their favorite
fruit.
(clear) (deffacts fruits (fruit apple 1) (fruit banana 1) (fruit orange 1)) (deftemplate person (slot name) (slot favorite-fruit)) (deffacts people (person (name Tom) (favorite-fruit apple)) (person (name Jenna) (favorite-fruit orange)) (person (name Rian) (favorite-fruit apple))) (defrule people-eat-favorite-fruits (person (name ?name) (favorite-fruit ?fruit)) ?f <- (fruit ?fruit ?id) => (println ?name " eats " ?fruit) (retract ?f)) (defrule persons-favorite-fruit-missing (person (name ?name) (favorite-fruit ?fruit)) (not (fruit ?fruit ?)) => (format t "There's no more of %s's favorite fruit %s!%n" ?name ?fruit)) (reset) (assert (fruit apple 2) (fruit orange 2)) (println "Run 1:") (println "------") (run) (println " ") (reset) (println "Run 2:") (println "------") (run)
- ← 17. Templatize me
- 18. Clearly resetting
- 19. Who watches the statistics? →
19. Who watches the statistics?
(clear) (watch statistics) (defrule fibonacci ?fact <- (fib ?fib ?lastfib ?i&:(> ?i 1)) => (print ?fib " ") (retract ?fact) (assert (fib (+ ?fib ?lastfib) ?fib (- ?i 1)))) (defrule last-fibonacci-number ?fact <- (fib ?fib ? 1) => (println ?fib) (retract ?fact)) (assert (fib 1 0 10)) (run) (unwatch statistics)
- ← 18. Clearly resetting
- 19. Who watches the statistics?
- 20. Rules: Activate! →
20. Rules: Activate!
(clear) (watch activations) (defrule b-exists-and-a-less-than-1 (b ?b) (a ?a&:(< ?a 1)) => (println "there is b = " ?b ", AND a " ?a " is less than 1")) (defrule a-less-than-1 (a ?a&:(< ?a 1)) => (println "a " ?a " is less than 1")) (println " Step 1 ") (println "==========") (println "--- assert") (assert (a 2)) (println "------ run") (run) (println " ") (println " Step 2 ") (println "==========") (println "--- assert") (assert (a 0)) (println "------ run") (run) (println " ") (println " Step 3 ") (println "==========") (println "--- assert") (assert (b foo) (a -1) (a 3)) (println "------ run") (run) (unwatch activations)
- ← 19. Who watches the statistics?
- 20. Rules: Activate!
- 21. Watching functions →
21. Watching functions
(clear) (watch deffunctions) (deffunction less-than-1 (?a) (println "calculating whether " ?a " is less than 1...") (< ?a 1)) (defrule b-exists-and-a-less-than-1 (b ?b) (a ?a&:(less-than-1 ?a)) => (println "there is b = " ?b ", AND a " ?a " is less than 1")) (defrule a-less-than-1 (a ?a&:(less-than-1 ?a)) => (println "a " ?a " is less than 1")) (println " Step 1 ") (println "==========") (println "--- assert") (assert (a 2)) (println "------ run") (run) (println " ") (println " Step 2 ") (println "==========") (println "--- assert") (assert (a 0)) (println "------ run") (run) (println " ") (println " Step 3 ") (println "==========") (println "--- assert") (assert (b foo) (a -1) (a 3)) (println "------ run") (run) (unwatch deffunctions)
- ← 20. Rules: Activate!
- 21. Watching functions
- 22. COOL! →
22. COOL!
CLIPS has an Object Oriented Language built-in to it called COOL. It's an awesome name for a really convenient set of functionality.
Instead of deftemplates, you use defclasses.
Instead of assert, you use make-instance of.
Instances are kind-of like facts but with inheritance. You can still
match in the LHF of Rules just like with Facts.
Additionally, Instances can have a name which you can use to look up instances directly.
(clear) (defclass FOO (is-a USER) (slot bar)) (defclass BAZ (is-a FOO) (slot bat)) (defrule foo (object (is-a FOO) (bar ?bar)) => (println "Bar: " ?bar)) (make-instance of FOO (bar "My bar!")) (make-instance [ryjo] of BAZ (bar "Another one :)")) (run)
- ← 21. Watching functions
- 22. COOL!
- 23. ??? →
23. ???
That's it for now! Thanks for sticking with the tutorial up to this point. I'll be adding to this over time, so make sure to bookmark this page and check back :).
All of the prose and code written in Chapters is authored by me. I used an LLM to create the scripts that format the markdown files and compile them into static HTML. I took a lot of inspiration from the Tour of Go when creating this tutorial. I found it super helpful to not need to install anything when learning Go years back. I used emscripten to compile CLIPS into a WASM binary. I wrote the code editor + syntax highlighter with code snippets pulled from Stackoverflow. I'll admit I'm a bit proud of that :).
My goal with this tutorial is to spread the word about CLIPS. I'd like to see it used in more everyday applications because it's a very powerful way of expressing complex business logic with less lines of code.
(clear) (run)
- ← 22. COOL!
- 23. ???