http://blog.athico.com/2014/07/drools-executable-model.html
Drools Executable Model (Rules in pure Java)
Posted by Mario Fusco
The Executable Model is a re-design of the Drools lowest level model handled by the
engine. In the current series (up to 6.x) the executable model has grown organically over
the last 8 years, and was never really intended to be targeted by end users. Those wishing
to programmatically write rules were advised to do it via code generation and target drl;
which was no ideal. There was never any drive to make this more accessible to end users,
because extensive use of anonymous classes in Java was unwieldy. With Java 8 and
Lambda's this changes, and the opportunity to make a more compelling model that is
accessible to end users becomes possible.
This new model is generated during the compilation process of higher level languages, but
can also be used on its own. The goal is for this Executable Model to be self contained
and avoid the need for any further byte code munging (analysis, transformation or
generation); From this model's perspective, everything is provided either by the code
or by higher level language layers. For example indexes etc must be provided by arguments,
which the higher level language generates through analysis, when it targets the Executable
model.
It is designed to map well to a Fluent level builders, leveraging Java 8's lambdas.
This will make it more appealing to java developers, and language developers. Also this
will allow low level engine feature design and testing, independent of any language. Which
means we can innovate at an engine level, without having to worry about the language
layer.
The Executable Model should be generic enough to map into multiple domains. It will be a
low level dataflow model in which you can address functional reactive programming models,
but still usable to build a rule based system out of it too.
The following example provides a first view of the fluent DSL used to build the executable
model
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DataSource persons = sourceOf(new Person("Mark", 37),
new Person("Edson", 35),
new Person("Mario", 40));
Variable<Person> markV = bind(typeOf(Person.class));
Rule rule = rule("Print age of persons named Mark")
.view(
input(markV, () -> persons),
expr(markV, person -> person.getName().equals("Mark"))
)
.then(
on(markV).execute(mark -> System.out.println(mark.getAge())
)
);
The previous code defines a DataSource containing a few person instances and declares the
Variable markV of type Person. The rule itself contains the usual two parts: the LHS is
defined by the set of inputs and expressions passed to the view() method, while the RHS is
the action defined by the lambda expression passed to the then() method.
Analyzing the LHS in more detail, the statement
?
1
input(markV, () -> persons)
binds the objects from the persons DataSource to the markV variable, pattern matching by
the object class. In this sense the DataSource can be thought as the equivalent of a
Drools entry-point.
Conversely the expression
?
1
expr(markV, person -> person.getName().equals("Mark"))
uses a Predicate to define a condition that the object bound to the markV Variable has to
satisfy in order to be successfully matched by the engine. Note that, as anticipated, the
evaluation of the pattern matching is not performed by a constraint generated as a result
of any sort of analysis or compilation process, but it's merely executed by applying
the lambda expression implementing the predicate ( in this case, person ->
person.getName().equals("Mark") ) to the object to be matched. In other terms
the former DSL produces the executable model of a rule that is equivalent to the one
resulting from the parsing of the following drl.
?
1
2
3
4
5
6
rule "Print age of persons named Mark"
when
markV : Person( name == "Mark" ) from entry-point "persons"
then
System.out.println(markV.getAge());
end
It is also under development a rete builder that can be fed with the rules defined with
this DSL. In particular it is possible to add these rules to a CanonicalKieBase and then
to create KieSessions from it as for any other normal KieBase.
?
1
2
3
4
5
CanonicalKieBase kieBase = new CanonicalKieBase();
kieBase.addRules(rule);
KieSession ksession = kieBase.newKieSession();
ksession.fireAllRules();
Of course the DSL also allows to define more complex conditions like joins:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Variable<Person> markV = bind(typeOf(Person.class));
Variable<Person> olderV = bind(typeOf(Person.class));
Rule rule = rule("Find persons older than Mark")
.view(
input(markV, () -> persons),
input(olderV, () -> persons),
expr(markV, mark -> mark.getName().equals("Mark")),
expr(olderV, markV, (older, mark) -> older.getAge() > mark.getAge())
)
.then(
on(olderV, markV)
.execute((p1, p2) -> System.out.println(p1.getName() + " is older
than " + p2.getName())
)
);
or existential patterns:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Variable<Person> oldestV = bind(typeOf(Person.class));
Variable<Person> otherV = bind(typeOf(Person.class));
Rule rule = rule("Find oldest person")
.view(
input(oldestV, () -> persons),
input(otherV, () -> persons),
not(otherV, oldestV, (p1, p2) -> p1.getAge() > p2.getAge())
)
.then(
on(oldestV)
.execute(p -> System.out.println("Oldest person is " +
p.getName())
)
);
Here the not() stands for the negation of any expression, so the form used above is
actually only a shortcut for
?
1
not( expr( otherV, oldestV, (p1, p2) -> p1.getAge() > p2.getAge() ) )
Also accumulate is already supported in the following form:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Variable<Person> person = bind(typeOf(Person.class));
Variable<Integer> resultSum = bind(typeOf(Integer.class));
Variable<Double> resultAvg = bind(typeOf(Double.class));
Rule rule = rule("Calculate sum and avg of all persons having a name starting with
M")
.view(
input(person, () -> persons),
accumulate(expr(person, p -> p.getName().startsWith("M")),
sum(Person::getAge).as(resultSum),
avg(Person::getAge).as(resultAvg))
)
.then(
on(resultSum, resultAvg)
.execute((sum, avg) -> result.value = "total = " + sum +
"; average = " + avg)
);
To provide one last more complete use case, the executable model of the classical fire and
alarm example can be defined with this DSL as it follows.
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Variable<Room> room = any(Room.class);
Variable<Fire> fire = any(Fire.class);
Variable<Sprinkler> sprinkler = any(Sprinkler.class);
Variable<Alarm> alarm = any(Alarm.class);
Rule r1 = rule("When there is a fire turn on the sprinkler")
.view(
input(fire),
input(sprinkler),
expr(sprinkler, s -> !s.isOn()),
expr(sprinkler, fire, (s, f) -> s.getRoom().equals(f.getRoom()))
)
.then(
on(sprinkler)
.execute(s -> {
System.out.println("Turn on the sprinkler for room " +
s.getRoom().getName());
s.setOn(true);
})
.update(sprinkler, "on")
);
Rule r2 = rule("When the fire is gone turn off the sprinkler")
.view(
input(sprinkler),
expr(sprinkler, Sprinkler::isOn),
input(fire),
not(fire, sprinkler, (f, s) -> f.getRoom().equals(s.getRoom()))
)
.then(
on(sprinkler)
.execute(s -> {
System.out.println("Turn off the sprinkler for room " +
s.getRoom().getName());
s.setOn(false);
})
.update(sprinkler, "on")
);
Rule r3 = rule("Raise the alarm when we have one or more fires")
.view(
input(fire),
exists(fire)
)
.then(
execute(() -> System.out.println("Raise the alarm"))
.insert(() -> new Alarm())
);
Rule r4 = rule("Lower the alarm when all the fires have gone")
.view(
input(fire),
not(fire),
input(alarm)
)
.then(
execute(() -> System.out.println("Lower the alarm"))
.delete(alarm)
);
Rule r5 = rule("Status output when things are ok")
.view(
input(alarm),
not(alarm),
input(sprinkler),
not(sprinkler, Sprinkler::isOn)
)
.then(
execute(() -> System.out.println("Everything is ok"))
);
CanonicalKieBase kieBase = new CanonicalKieBase();
kieBase.addRules(r1, r2, r3, r4, r5);
KieSession ksession = kieBase.newKieSession();
// phase 1
Room room1 = new Room("Room 1");
ksession.insert(room1);
FactHandle fireFact1 = ksession.insert(new Fire(room1));
ksession.fireAllRules();
// phase 2
Sprinkler sprinkler1 = new Sprinkler(room1);
ksession.insert(sprinkler1);
ksession.fireAllRules();
assertTrue(sprinkler1.isOn());
// phase 3
ksession.delete(fireFact1);
ksession.fireAllRules();
In this example it's possible to note a few more things:
Some repetitions are necessary to bind the parameters of an expression to the formal
parameters of the lambda expression evaluating it. Hopefully it will be possible to
overcome this issue using the -parameters compilation argument when this JDK bug will be
resolved.
any(Room.class) is a shortcut for bind(typeOf(Room.class))
The inputs don't declare a DataSource. This is a shortcut to state that those objects
come from a default empty DataSource (corresponding to the Drools default entry-point). In
fact in this example the facts are programmatically inserted into the KieSession.
Using an input without providing any expression for that input is actually a shortcut for
input(alarm), expr(alarm, a -> true)
In the same way an existential pattern without any condition like not(fire) is another
shortcut for not( expr( fire, f -> true ) )
Java 8 syntax also allows to define a predicate as a method reference accessing a boolean
property of a fact like in expr(sprinkler, Sprinkler::isOn)
The RHS, together with the block of code to be executed, also provides a fluent interface
to define the working memory actions (inserts/updates/deletes) that have to be performed
when the rule is fired. In particular the update also gets a varargs of Strings reporting
the name of the properties changed in the updated fact like in update(sprinkler,
"on"). Once again this information has to be explicitly provided because the
executable model has to be created without the need of any code analysis.