Enterprise Java

Drools Executable Model

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:

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:

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:

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.

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.

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:

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:

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:

not( expr( otherV, oldestV, (p1, p2) -> p1.getAge() > p2.getAge() ) )

Also accumulate is already supported in the following form:

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.

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.
Reference: Drools Executable Model from our JCG partner Mario Fusco at the Drools & jBPM blog.

Mario Fusco

Drools Expert
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button