-
Notifications
You must be signed in to change notification settings - Fork 115
Guide
This guide shows the syntax and examples of using Clara.
Rules are typically defined with defrule, which has this structure:
A simple rule looks like this:
(defrule free-lunch-with-gizmo
"Anyone who purchases a gizmo gets a free lunch."
[Purchase (= item :gizmo)]
=>
(insert! (->Promotion :free-lunch-with-gizmo :lunch)))
Where Purchase
is a Clojure Record or Java JavaBean containing an item
field. If there exists a purchase that matches the :gizmo
keyword, a new promotion is inserted into working memory, by simply creating a new Promotion record.
The left-hand side of the rule — everything prior to the =>
in the above — uses a constraint expression described in the Constraint Expressions section below.
The right-hand side of the rule — everything past the =>
in the above example — is simply a Clojure S-expression and can invoke arbitrary code, or use insert!
to insert new information into the working memory.
All variables bound on the left-hand side are visible on the right-hand side, and can be used in arbitrary Clojure code there. In addition, a specialize binding called ?__token__
, which is a record that includes facts — a sequence of working-memory elements — and bindings, which is a map of the bindings above. Typically this won't be needed for most use cases, but does offer context on why the rule fired.
Queries are typically defined with defquery
, which has the following structure:
A sample query looks like this:
(defquery get-promotions
"Query to find promotions for the purchase."
[]
[?promotion <- Promotion])
The first argument is a vector of keywords to indicate parameters to a query. For instance, if we wanted to run a query that retrieves only a certain type of promotions, we might write this:
(defquery get-promotions
"Query to find promotions for the purchase."
[:?type]
[?promotion <- Promotion (== ?type type)]) ; Bind the ?type query to the promotion type.
A caller may then execute that query with arguments. So if we only wanted to find lunch promotions, we might perform the query like this:
(query session get-promotions :?type :lunch)
The conditions used by a query are the same structure as the left-hand side of a rule. See the Condition Expressions section below for usage.
Condition Expressions are the contents of the left-hand side of rules, or the constraints used in queries.
This part of rules and queries contains a series of expressions, each of which 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.
A key part of any rule engine is the ability to bind values to variables and unify variables to ensure all items described in a rule are consistent.
In Clara, bindable variables are prefixed with a question mark. As of 0.2, any comparison of a bindable variable (using =
or ==
) to a fact variable, will bind that value, and ensure all references to the bound variable are consistent across all conditions in the rule. Variables can also be bound using an assignment-style variable, <-
. See the Fact expressions section below for details.
Bound variables can be referenced in tests and the right-hand side of rules, by simply using them as an in-scope Clojure variable.
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.
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.
Starting in 0.2.0, 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 are simply prefix-style boolean operations over fact expressions, or nested boolean expressions. Clara prefers 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.
Accumulators are used to reason over collections of facts, much like how we might use map
, reduce
and similar operations in Clojure. They are similar to the concept of the same name in Jess and Drools.
See the accumulators page for details on writing accumulators, but here we show an example of using a built-in accumulator:
;; Creates an accumulator that selects the item with the newest timestamp field.
(def newest-temp (acc/max :timestamp :returns-fact true))
(defrule get-current-temperature
[?current-temp <- newest-temp :from [Temperature (== ?location location)]]
=>
; Do something.
)
Accumulators may also be used to find minimum, maximum, average values, or do other summarization or selection for a set of matching records.
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)))
- Introduction to Clara
- Architecture overview
- ClojureScript support
- Using Clara from the REPL
- Clara Examples
- Full railroad diagram of Clara syntax
- Clojure API Documentation
- Java API Documentation
Railroad diagrams generated with http://railroad.my28msec.com/rr/ui