Ferrari

Armand du Plessis edited this page Jul 8, 2015 · 36 revisions

Ferrari is one of the syntaxes in The Ruleby DSL. It defines four parameters for the ‘rule’ method:


  • An optional Symbol that is a unique identifier for the rule
  • An optional Hash that contains rule attributes (such as priority)
  • A variable number of Array s representing patterns (the “left hand side”)
  • A block that will be executed if the rule is satisfied (the “right hand side”)

The following is an example of a rule using this syntax:

rule :hello, [Message, :m, method.status == :HELLO] do |context|
puts context[:m].message
end

This rule defines a pattern that looks for the existence of a Message object with a status property that is equal to :HELLO. The block parameter will be executed for each object that this pattern matches.

The Left Hand Side

The most important part of this example is the content of the Array parameter, which represents the left hand side of the rule. Each Array begins with a class type, and is followed by an optional handle for the object (so that it can be referenced in the block). Next, the Array contains any number of conditions that must be true in order for the pattern to match. The example above contains only one condition:

method.status == :HELLO

The method keyword is used to identify that we are checking the value of the status method. However, this is the explicit form. The condition above could be shortened to:
m.status == :HELLO

There are several shortcuts like this one in Ruleby. The reason for having both the long and short versions is give the developer the ability to make the condition more human readable.

Bindings

In the example above, we could have bound our status value to a variable name. To do this we create a Hash with our condition as the key, and the variable name as the value:

{ m.status == :HELLO => :s }

This allows the :s variable to be referenced later on in the rule.

Conditions

There are a number of ways to write conditions in Ferrari. The example above is the simplest as it uses the == operator. Ferrari supports the following operators:


  • == and not==
  • < and <=
  • > and >=

The not== operator is necessary because overriding the != operator in Ruby is not allowed. All of these are operators are ‘shortcuts’ for conditions. If we have a condition that is more complicated, we can pass a block parameter.

m.status( &condition{ |s| s + ‘my_suffix’ = @some_var } )

The only requirement is that the block must evaluate to true or false. The condition method is really just an alias for the ‘lambda’ method in Kernel. But the developer can shorten this to just the c method if desired (this is demonstrated below).

We can also reference a bound variable:

m.status == binding(:some_var)

In this example, we use the binding method to reference the variable we bound to the :name symbol. The short hand version of this is:
m.status == b(:some_var)

Or we can do both:
m.status( :some_var, &c{ |s,v| v == s + ‘my_suffix’ } )

The Right Hand Side

The block parameter to the rule method is less complicated than the left hand side. It is simply a block of Ruby code that will be executed each time the rule is matched. This block has two parameters:

  1. engine – a handle to the Engine object (used for asserting and retracting facts)
  2. context – a Hash of bound variables that can be used in the block.

Of course, these parameters can be named whatever the developer desires to call them.

Rule Attributes

The Hash that contains the rule attributes typically looks something like the following example:

{ :priority => 5 }

At this time, the only rule attribute that Ruleby supports is :priority (a.k.a. ‘salience’).

Rule with larger priority number runs first.

Quantifiers

The class value in the Array (left hand side parameter) can optionally be preceded by a quantifier. Valid quantifiers are as follows:


  • :~
  • :not
  • :exists
  • :is_a?

The :~ and :not quantifiers are semantically the same. They are existential quantifiers that check for the non existence of a fact in working memory.

The :exists quantifier is an existential quantifier that checks for the existence of something in working memory. With this qualifier, the rule’s action will only execute once regardless of how many facts in working memory match the pattern.

The :is_a? quantifier checks for facts that are of the type specified. Without this quantifier, only facts with the exact type specified will match the rule.

If no quantifier is specified, the default action is for the rule to execute the right hand side for every fact in working memory that matches the pattern.

Conditional Elements


In the examples show above, there is an implicit AND conditional element between the patterns. A rule will not be fired unless all the patterns are true. Ruleby also provides the capability of specifying both an explicit AND conditional element and an explicit OR conditional element on the LHS.

Consider this example of the OR conditional element:

rule OR ([Message, m.message == :FIRST], [Message, m.message == :SECOND]) do |v|
puts ‘first or second’
end

If only Message(:FIRST) is in working memory, this rule will be fired once. The same goes if on Message(:SECOND) is in working memory. But if both Message(:FIRST) and Message(:SECOND) are in working memory, the rule with the OR conditional element will be fired twice. This is because the rule above is equivalent to the following two rules:
rule [Message, m.message == :FIRST] do |v|
puts ‘first or second’
end

rule [Message, m.message == :SECOND] do |v|
puts ‘first or second’
end


If you only want the rule to fire once, a common idiom is to retract the facts from working memory in the LHS:
rule OR ([Message, :m, m.message == :FIRST], [Message, :m, m.message == :SECOND]) do |v|
puts ‘first or second’
retract v[:m]
end

Note in the example above that both Message(:FIRST) and Message(:SECOND) are bound to the name :m. Ruleby provides this ability to do conditional binding. But there is no guarantee as to which pattern was matched.

Because Ruleby supports both the AND and OR conditional elements. It is possible to nest conditionals inside each other. For example:

rule OR (AND (OR ([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Message, m.message == :THIRD])) do |v|
puts ‘first or second, and third’
end