In the last post, I introduced logic operators in boolean rules. Drools, in its standard form, supports AND and, in a limited way, OR. In fact, these operators are sufficient to write a number of rules. The addition of the other common logic operators (XOR, EQUIV,IMPLY) is more syntactic sugar than a real valuable feature – in fact, no rule engine supports them openly.
In presence of imperfection, instead, much changes. A degree is more than a simple boolean and thus carries additional information that can be combined in complex ways. Let’s take the conjuction AND as an example. The common, general idea is that the result should tend to true (whatever true means) the more all the operands tend to true individually.
In practice this is a vague constraint that leaves many degrees of freedom.
Consider the basic case: imperfection is used to model fuzziness, and real numbers are used as degrees. This is perhaps the simplest case, since operators are truth-functional (i.e. they just require the degrees of their operands to be evaluated) and degrees themselves are extremely simple.
The logic conjunction of two degrees can be obtained by taking their minimum:
1) d(A && B) = min( d(A) , d(B) )
but also their product:
2) d(A && B) = d(A) * d(B)
or again:
3) d(A && B) = max( 0 , d(A) + d(B) -1 )
These operations (technically called t-norms) are but some – fundamental – examples of a whole family of operations, all of which are candidate implementation of the AND operator.
Things do not improve much if one chooses different types of imperfection: take, for example, probability. Supposing, again, that real values model the probability of truth (thus putting ourselves in the simplest probabilistic case), AND can be implemented by taking the product of the operand probabilities – BUT only if the operands are conditionally independent. If that is not the case, the operator will not be truth-functional and thus will have to perform more complicated calculations, possibly argument-dependent:
4) p(A && B) = p(A) * p(B|A)
Similar concepts apply to the other operators. An operator, then, is actually an abstract construct that can be customized and configured. To do so, attributes can be attached to each individual operator, choosing one or more among the following:
- id : an identifier which can be used to reference the operator
- kind : a string selecting a specific implementation of the operator
- args : a string containing additional information required to configure the operator
Perhaps the best way to understand them is to imagine the following call:
ID = new OperatorKind(Args)
In fact, a centralized factory is used to instantiate the operators during ther construction of the RETE network: it uses the value of kind to choose the concrete classes and args to provide arguments to the constructors. Obviously, the factory can be configured with a default type to return if no kind is specified explicitly.
These attributes can be attached to operators, both within and between Patterns, and also to pattern themselves. The reason is simple: a pattern
Type ( constraints )
is transformed into the conjuction
object.class == Type && constraints
so the attributes are attached to the hidden conjunction.
The exact syntax is shown in the following example (attributes are optional) :
rule "Annotated_Ops_Example"
when
Type1(
field1 == "a"
op_within @( id="..." kind="..." args="..." )
field2 == "b"
)
op_between @( id="..." )
Type2(
field3 == "c"
) @( kind="..." args="..." )
then
...
The symbol “@” is used to introduce the metadata between the brackets, which, in the specific case, are given by the pairs attribute/value.