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) |
Conversely the expression
1 | expr(markV, person -> person.getName().equals( "Mark" )) |
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 |
1 2 3 4 5 | CanonicalKieBase kieBase = new CanonicalKieBase(); kieBase.addRules(rule); KieSession ksession = kieBase.newKieSession(); ksession.fireAllRules(); |
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()) ) ); |
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()) ) ); |
1 | not( expr( otherV, oldestV, (p1, p2) -> p1.getAge() > p2.getAge() ) ) |
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) ); |
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(); |
- 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.