Writing Expressions

A query or rule left-hand side is a sequence of conditions, all of which must be satisfied for the rule or query to match. Each condition is one of the following:

  • A fact expression, which selects a fact based on some given criteria.
  • A boolean expression, which is simply a hierarchical and/or/not structure of other expressions.
  • An accumulator, which is mechanism to reason about collections of facts
  • A test, which is an arbitrary Clojure S-expression that can run predicate-style tests on variables bound earlier in the rule or query.

Details on each of these are below.

Fact Expressions

Fact expresions use the following structure:

fact expression

Fact expressions are the simplest and most common form of Clara conditions. They start with an optional binding for the fact, in the form of [?variableName <- MyFactType]. The fact type is then followed by zero or more S-expressions which can either be predicates run against a fact, or bindings, as described above.

A simple fact expression may look like this:

[?person <- Person (= first-name "Alice") (= ?last-name last-name)]

This example does the following:

  • Matches all facts of type Person
  • Eliminates any Person facts that don’t have a first-name attribute that is equal to “Alice”
  • Creates a new binding ?last-name, which contains the value of the last-name field in matched Person facts.
  • Creates a new binding ?person, which contains the value of the matched Person object.

Let’s look at how this variable binding works.

Variable Bindings

Any symbol that starts with a question mark is a variable. Variables are bound either as an entire fact (like the [?person <- Person] example above), or as part of an expression in the form of (= ?variable-name some-value-or-expression).

Variables are unified across all conditions in a rule. If the same binding is used in multiple conditions, Clara ensures that all conditions can be satisfied with the same binding before activating the rule or query.

Fact Types and Destructuring

If you plan on using Java Beans or Clojure Records for your facts, you can probably skip this section. As seen in the above examples, Clara simply matches on the bean or record type and makes the fields available to constraint expressions.

While Clojure Records or Java Beans are good for many needs, some use cases ask that Clara rules be written against arbitrary Clojure structures. Clara supports that with two features shown below:

  • Users may provide a function to determine the logical type of an incoming fact.
  • Users may use Clojure destructuring within a rule to easily access data in the rule’s constraints.

Here we look at each of these.

Fact Types

The first element of a fact expression is the fact type, the logical type of the object that matches the condition. Clara uses Clojure’s type function by default to determine the type of an object. The rule itself will match if it uses that type or any ancestor of it.

The strategy for identifying the logical type can be overridden by passing a :fact-type-fn option when creating the session. For instance, if a session is created in the following way:

(mk-session 'example.rule :fact-type-fn :request-type)

Then the caller may insert and match objects like this:

{:request-type :get :url "http://example.com/"} ; Matches rules of type :get
{:request-type :put :url "http://example.com"} ; Matches rules of type :put

This way arbitrary maps can be used as Clara facts, provided a function can be specified that returns the logical type given a map.

Destructuring Facts

Facts matching a condition can be arbitrary Clojure maps and destructured using Clojure’s destructuring mechanism. For instance, suppose person contained an address and we were interested in the city. We might do something like this:

[Person [{{city :city state :state} :address}] (= ?city city)]

Note that destructuring itself is optional; in its simplest form this could be used just to bind the fact as an argument, just as we would in a function call. For instance:

[Person [person] (= ?city (get-in person [:address :city]))]

Does the same, binding person as the fact argument and simply accessing the nested fields.

If no destructuring block is provided at all, then the default destructuring simply exposes the name of each field of the type to the constraints. Clara fields are simply record fields if the fact is a Clojure record, or Java Bean properties in the case of a bean.

Boolean Expressions

boolean expression

Boolean expressions are simply prefix-style boolean operations over fact expressions, or nested boolean expressions. Clara requires the use of keyword ‘:and’, :or’, and ‘:not’ for its boolean expressions to keep clear what expressions are part of a Rete network as opposed to a normal Clojure expression.

An example boolean expression may look like this:

[:or [Customer (= status :vip)]
     [Promotion (= type :discount-month)]]

This will generate a rule that fires if the Customer fact has a vip status, or there is a promotion of type discount month.

:and, :or, and :not operations can be nested as one would expect.

For further information, see Boolean Expressions.

Tests

test expression

Tests in clara are simple predicates that can be run over variables bound earlier in the rule or query. For example:

(defrule is-older-than
   [Person (= ?name1 name) (= ?age1 age)]
   [Person (= ?name2 name) (= ?age2 age)]
   [:test (> ?age1 ?age2)]
   =>
   (println (str ?name1 "is older than" ?name2)))

What’s next?

  • Accumulators are used to aggregate or work with collections of matching facts.