The Inference Task
This column is the next in a series that provides the reader with best practices on using or choosing a rules engine. The target audience for this series is typically the user of a rule engine, i.e., a programmer or someone with programming skills. All coding examples should be read as pseudo-code and should be easily translated to a specific target syntax for a rule engine that supports backward and forward chaining in an object-oriented environment.
We will discuss recommendations on how to create a rule service that can infer new information based on existing information with rules using a rules engine. We will call the task that the program performs an inference task. In this description the following concepts are important:
- Rule set: a collection of rules (or rule sets) that are grouped
The grouping can be done in various ways.
- Infer block: a construct that uses rules to solve a problem
The solution algorithm may be goal driven (backward chaining) or data driven (forward chaining).
- Inference Task: a set of methods that are used to perform a task, using one or more infer blocks
Besides the execution of the infer block it also takes care of the preparation and the processing of the results of the infer block.
In this column I want to emphasize the importance of keeping the infer blocks 'clean'. This means that the infer block applies the following template:
| example code
infer [history ]
The reason for doing this is that within an infer block the application 'enters a different world' of the application. The infer statement creates an instance of the Inference Engine and you don't want to do anything else than 'inferencing' as long as the engine is active.
Definition of inference task
The definition of 'inference task' is given above. In the following discussion I may simply refer to this as 'task'.
The task usually maps to a solution task in your design — more specifically: a solution with the help of rules. The inference task is the direct context of an infer block. It usually takes care of the preparation of the case data, and it wraps up the results of the infer block(s).
I recommend that each inference task be represented as a class — a class that has a single (public) entry point, for example, 'Perform( )'. This method has a generic implementation:
| example code
inference task perform method template
if current.precondition (1)
Here is an explanation of the code lines above:
- process results
- when precondition failed
Do some checks for the availability of instances and/or attribute values. If you want a special method for determination of whether or not the task is eligible to be performed, you can introduce a public class method called 'IsEligibleToPerform'. This method is evaluated before you create an instance of the task.
Prepare the inference. This method may collect extra data from the database.
The actual inference: this method contains the infer block. I deliberately keep the infer statement out of this generic method because some tasks may require the use of the history option, while others don't.
Wrap up the results of the chaining process; check the post-condition(s) of the task.
Perform error handling and/or logging
It is a good idea to have a notion of the 'client' of the task. This client is passed to the task as a pointer to an interface class. The pointer may be passed with the Create( ) method of the task class.
The 'Task Client Interface' defines the methods that the caller of the task has to implement. These methods include, but are not limited to, functions to retrieve extra data, perform error handling or logging, and handle the results of the task.
Here are some examples of the TaskClientInterface methods:
- WhenPreconditionFailed(in message is string)
- WhenPostConditionFailed(in message is string)
- WhenError(in errnum is TTaskError)
- CollectSalaryDataForPeriod(in date_start is date, in date_end is date)
# # #