collect keyword allows us to gather information directly from a source into a collection. This is a very useful component when we already have the elements as we need them to be somewhere accessible in the working memory. However, sometimes we need to apply transformations to the data that matches a condition. For these cases, the
accumulate keyword is used to transform every match of a condition to a specific type of data.
You can think of the
accumulate keyword as a
collect on steroids. You not only get to group every item that matches a condition, but also extrapolate the data from these elements in a specific programmatic way. Some common examples where the accumulate keyword is used are counting elements that match a specific condition, get average values of an attribute of certain type of objects, and to find the average, minimum, or maximum values of a specific attribute in a condition.
Perhaps an example could help clarify the use of accumulate. In the previous rules, we’ve used a method of the Order object, called
getTotalItems, to return the count of items that we were purchasing in this order. So far, this has been the only way we would have to get this information from the Order object. However, using accumulate, we can obtain this information while filtering specific items using the full power of rules. Let’s see this in an example:
rule "10+ items with sale price over 20 get discount" when $o: Order($lines: orderLines) Number(intValue >= 10) from accumulate(OrderLine(item.salePrice > 20.00, $q: quantity) from $lines, init(int count = 0;), action(count += $q), reverse(count -= $q), result(count)) then ... end
We’ve got a lot to explain from the previous rule. It’s main objective is to get the total number of items in the order that follows a specific condition: having a sale price value higher than 20. The getTotalItems method would be able to give us a count of all the items, however, it wouldn’t be able to filter through them. The accumulate here, however, would allow us to apply a condition to the elements and apply a predicate to transform the elements that match the accumulate condition in the information that we need. Let’s analyze the parts of the accumulate to fully understand its structure, as follows:
- $o: Order($lines: orderLines): This part is simple. We’re checking every Order object that we have in our working memory and storing the order lines attribute in a variable for later use.
- Number(intValue >= 10): Our rule is trying to check whether we have at least, 10 items with price higher than 20. This first part of the accumulate line is checking the return value from the accumulate. It can be anything that you want, but for this first example, it is going to be a number. If that number is 10 or more, we consider the return value of the accumulate to be fulfilling our rule. This means that the accumulate should count all the items that match the specific condition.
- Condition inside accumulate: In the accumulate, we have a first part that matches a specific condition. In this case, it goes all over the OrderLine objects of the order (which were stored in the
$linesvariable), checks whether the sale price is over 20 and stores the quantity attribute value in a $q variable to be used later. Notice how you have a second
fromclause in the accumulate as you can nest them when needed.
The following four parts of the accumulate (init, action, reverse, and result) contain information about what to do whenever we find an object that matches the condition:
- init: This is done at the beginning of the accumulate. It initializes the variables to be used every time we find a match in the accumulate condition. In our case, we’re initializing a count variable.
- action: Whenever we find an element in the working memory that matches the accumulate condition, the rule engine will trigger the action section of code in the accumulate. In our example, we’re increasing the count of the elements by the value of the $q variable, which holds the quantity of the OrderLine object.
- reverse: This is an optional code block. As every object inserted, modified, or deleted from the working memory might trigger the condition (and activate the action code block), it can also make an element that had already matched the accumulate condition to stop doing so. In order to increase the performance, the reverse code block can be provided to avoid having to recalculate all the accumulate again if it is too processor-intensive. In our example, whenever OrderLine stops matching the condition, the reverse code will just decrease the count variable by the amount of the $q variable that is previously stored.
- result: This code block holds the return variable name or formula for the finished accumulate section. Once every object that matches a condition has been processed and all the
actioncode blocks have been called accordingly, the
resultcode block will return the specific data that we collected, which will dictate the type that we’ll write on the right-hand side of the
from accumulateexpression. In our example, we return the count value (an integer value), which is matched against the first part of the accumulate function:
Number(intValue > 10).
Although this is the full syntax for using
accumulate, it doesn’t necessarily have to be this complex every single time. Drools provides a set of predefined accumulate functions that you can directly use in your rules. Here’s the previous rule that is rewritten to show how to use one of these built-in functions:
rule "10+ items with sale price over 20 get discount" when $o: Order($lines: orderLines) Number(intValue >= 10) from accumulate(OrderLine(item.salePrice > 20.00, $q: quantity) from $lines, sum($q)) then ... end
The already provided accumulate functions are as follows:
- count: This keeps a count of a variable that matches a condition
- sum: This sums up a variable, as shown in the previous example
- avg: This gets the average value of a variable for all the matches in the accumulate
- min: From all the variable values obtained in the accumulate condition, this returns the minimal one
- max: From all the variable values obtained in the accumulate condition, this returns the maximum one
- collectList: This stores all the values of a specified variable in the condition in an ordered list and returns them at the end of the accumulate
- collectSet: This stores all the values of a specified variable in the condition in a unique elements’ set and returns them at the end of the accumulate
Using a predefined function is preferred as it makes the rule more readable and is less prone to errors. You can even use many of them at once, one after the other, separated by a comma, as shown in the following example:
rule "multi-function accumulate example" when accumulate(Order($total: total), $maximum: max($total), $minimum: min($total), $average: avg($total)) then //... end
The previous rule stores the maximum, minimum, and average of the totals of all the orders in the working memory. As you can see, the
from clause is not mandatory for the
accumulate keyword. It is even discouraged with multifunction accumulates as it might return completely different types of objects (such as