ImplementationHaving defined a formal interface, the implementation of the RBS is abstracted out. By reusing containers from the standard template library to store both the rulebase and the working memory, the reference implementation remains simple, too. Using a flexible file format (as described in the preceding section) means most of the preparations can be left to the developer. Notably, thanks to the rule priorities, the interpreter can perform a simple traversal of the rules—as suggested by the knowledge engineer. All the implementation is transparent from the interface, so extending it would usually not break the system. If we want to optimize the rules, for example, we can build a tree internally after the rules are loaded (although this would involve discarding the priorities).
Data StructuresOne of the essential concepts for maintaining an efficient implementation is to represent the symbols as integers rather than strings. Because all the interfaces deal with strings (for intuitiveness during initialization), we need an internal map to convert the strings to integer symbols. After this is done, the working memory can be a simple array, with the integer symbols used directly as the index in the working memory. The effectors and sensors are stored in two containers associating integer symbols with native functions passed by the interface. InterpreterThe main loop of the interpreter is split into three steps conceptually. First, CheckSensors() calls the native functions to set the appropriate symbols. Doing this before the matching phase guarantees the function is only called once. However, we could sacrifice a small amount of memory and opt for lazy evaluation instead (for instance, a "gathered" bit). The native functions are checked as they are needed by the rules, only if the symbol value is not up-to-date. Then, ApplyRules() scans through each of the rules in the order they were specified. Each condition in the rule head is checked. If one condition doesn't match, the rest are skipped and the next rule is used. If all the conditions match, the default values are applied. Only then is the rule body executed to override the defaults and set other symbols as appropriate. If no rules match, the defaults are set. Finally, CheckEffectors() scans only the effector symbols in the working memory and calls the native functions if any of them are true. Because we're using the RBS for control, it's more appropriate for the effectors to be checked every iteration—rather than when a rule is executed. This reduces the number of rules required. |