Source Discussion


Let's now look at a simple implementation of a rules-based system using the rules and fact format discussed thus far.

The basic flow of the software is shown in Figure 8.4. After initializing (clearing) the working memory and the rules-set, the rules file specified by the user is parsed and the rules loaded into the rules-set. The software then goes into a loop trying to match a rule given the current working memory. When a rule is matched, the execution logic is called to perform the consequents of the particular rule. Whether or not a rule was fired , the software continues by trying to match another rule. This is the basic simplified flow of the rules-based system.

click to expand
Figure 8.4: Basic flow of the rules-based system.

Let's first look at the data structures that are used by the source (Listing 8.3).

Listing 8.3: Rules-Based System Data Structures.
start example
 #define MEMORY_ELEMENT_SIZE   80 #define MAX_MEMORY_ELEMENTS   40 #define MAX_RULES             40 #define MAX_TIMERS            10 typedef struct memoryElementStruct *memPtr; typedef struct memoryElementStruct {   int active;   char element[MEMORY_ELEMENT_SIZE+1];   struct memoryElementStruct *next; } memoryElementType; typedef struct {   int active;   char ruleName[MEMORY_ELEMENT_SIZE+1];   memoryElementType *antecedent;   memoryElementType *consequent; } ruleType; typedef struct {   int active;   int expiration; } timerType; 
end example
 

The memoryElementType is the structure that is used not only to represent a fact in working memory, but also a rule in the rules-set. The element field holds the fact, in the working memory context. Type ruleType represents a single rule within the rules-set. The active word defines whether this rule is active in the system. The ruleName is the name of the rule (parsed after the "defrule" token). This is used primarily for debugging, but can also aid the knowledge-developer when dealing with a large number of rules. The antecedent and consequent elements represent linked-lists of terms. Recall that memoryElementType contains a next element; this either represents the address of the next term , or 0 to represent the end of the chain. The timerType represents a single timer. It again uses an active word to identify whether the timer is in use. The expiration field specifies when the timer should fire.

Listing 8.4 shows the declaration of the working variables used by the rules-based system. These are global variables used by the various subsystems in the software.

Listing 8.4: Global Structures Declaration.
start example
 memoryElementType workingMemory[MAX_MEMORY_ELEMENTS]; ruleType ruleSet[MAX_RULES]; timerType timers[MAX_TIMERS]; int endRun = 0; int debug = 0; 
end example
 

Declarations workingMemory , ruleSet , and timers were discussed with data structures (from Listing 8.3). Two new items are endRun , which allows the rule-set to end the run of the system (via the quit command), and debug , which specifies whether debugging information should be emitted .

The main function, as shown in Listing 8.5 provides the initialization of the system as well as loop (match-rule/fire). The function begins with parsing of the command-line using the getopt function. Three command-line parameters are supported, 'h' to emit the help information, 'd' to enable debugging and 'r' to specify the rules-file to use. Note that if no file is specified (the 'r' option is not found), then the help information is emitted by default and the software exits.

Listing 8.5: The Rules-Based System main Function.
start example
 int main( int argc, char *argv[] ) {   int opt, ret;   char inpfile[80]={0};   extern void processTimers( void );   extern int parseFile( char * );   extern void interpret( void );   while ((opt = getopt(argc, argv, "hdr:")) != -1) {     switch( opt ) {       case 'h':         emitHelp();         break;       case 'd':         debug = 1;         printf("Debugging enabled\n");         break;       case 'r':         strcpy(inpfile, optarg);         break;     }   }   if (inpfile[0] == 0) emitHelp();   bzero( (void *)workingMemory, sizeof(workingMemory) );   bzero( (void *)ruleSet, sizeof(ruleSet) );   bzero( (void *)timers, sizeof(timers) );   ret = parseFile( inpfile );   if (ret < 0) {     printf("\nCould not open file, or parse error\n\n");     exit(0);   }   while (1) {     interpret();     if (debug) {       printWorkingMemory();     }     processTimers();     if (endRun) break;     sleep(1);   }   return 0; } 
end example
 

The working structures in the system are then cleared, and based upon the active word specified in each record, all are clear. A call is then made to parseFile (see Listing 8.6) to read and parse the rules-file into the ruleSet structure.

Listing 8.6: The File Parsing Function ( parseFile ).
start example
 int parseFile( char *filename ) {   FILE *fp;   char *file, *cur;   int fail = 0;   extern int debug;   file = (char *)malloc(MAX_FILE_SIZE);   if (file == NULL) return -1;   fp = fopen(filename, "r");   if (fp == NULL) {     free(file);     return -1;   }   fread( file, MAX_FILE_SIZE, 1, fp);   cur = &file[0];   while (1) {     /* This will parse an entire rule */     /* Find the "(defrule" start of a rule */     cur = strstr( cur, "(defrule" );     if (cur == NULL) {       fail = 1;       break;     }     if (!strncmp(cur, "(defrule", 8)) {       int i=0;       cur+=9;       while (*cur != 0x0a) {         ruleSet[ruleIndex].ruleName[i++] = *cur++;       }       ruleSet[ruleIndex].ruleName[i++] = 0;       cur = skipWhiteSpace( cur );       /* Parse the antecedents */       cur = parseAntecedent( cur, &ruleSet[ruleIndex] );       if (cur == NULL) {         fail = 1;         break;       }       /* Should be sitting on the '=>' */       if (!strncmp(cur, "=>", 2)) {         cur = skipWhiteSpace( cur+2 );         /* Parse the consequents */         cur = parseConsequent( cur, &ruleSet[ruleIndex] );         if (cur == NULL) {           fail = 1;           break;         }         /* Ensure we're closing out the current rule */         if (*cur == ')') {           cur = skipWhiteSpace( cur+1 );         } else {           fail = 1;           break;         }       } else {          fail = 1;         break;       }       ruleSet[ruleIndex].active = 1;       ruleIndex++;     } else {       break;     }   }   if (debug) {     printf("Found %d rules\n", ruleIndex);   }   free( (void *)file );   fclose(fp);   return 0; } 
end example
 

The main loop is the final element shown in Listing 8.5. This loop performs the match/fire logic within the interpret function. If debugging is enabled, the working memory is emitted to follow along with the execution of rules and associated changes to working memory. The timers are processed through the call processTimers . If the 'quit' command was found, the loop is exited (via endRun ) and the program exits. Finally, to simulate timed-processing of the rules, a simple call to sleep is used (in addition to slowing the system down so that it can be viewed in operation). In a production system, this would be removed and the function processTimers would utilize the current time to fire off the necessary rule.

The file parser (see Listing 8.6) takes the rules stored in the file and parses them to rules containing antecedents and consequents. The parser is very simple since the structure of the file is simple. The rules file is made up of one or more rules. Figure 8.5 shows the format of each rule.

click to expand
Figure 8.5: Format of rules within the system.

The "defrule(" token starts off a new rule and is followed by a string name for the rule. One or more antecedents follow with the "=>" token separating the set of one or more consequents. The ")" symbol closes out the rule. Examples of rules following this form are shown in Listing 8.2.

Given this very regular and predictable structure, rules' parsing is performed using a predictive parser. The parser very simply searches for the start token and then parses the antecedents. Once the separator token is found, the consequents are parsed. Finally, the closing symbol is checked to ensure the rule had proper form. If everything passes , the loop continues looking for a new rule. Listing 8.6 also shows calls to the function skipWhiteSpace . This function is used to skip around comments, spaces, and other text that is not part of the rule.

Parsing the antecedents and consequents entails the use of two functions that parse each type (see Listing 8.7). Each function uses the parseElement function to parse the actual term from the file.

Listing 8.7: Antecedent and Consequent Parsing Functions.
start example
 char *parseAntecedent( char *block, ruleType *rule ) {   while (1) {     block = skipWhiteSpace( block );     if (*block == '(') {       block = parseElement( block, &rule->antecedent );     } else break;   }   return block; } char *parseConsequent( char *block, ruleType *rule ) {   while (1) {     block = skipWhiteSpace( block );     if (*block == '(') {       block = parseElement(block, &rule->consequent);     } else break;   }   return block; } 
end example
 

Note the use of skipWhiteSpace before element parsing in the two functions. This allows comments after each consequent or antecedent in the file.

The parseElement function pulls a fact from the file, taking care to match the parenthesis (since they're may be one pair, or two, depending upon the type of consequent fact). The function uses the parenthesis as state variables to know when a term has been completely parsed (and stored). Once the element has been parsed, it is added to the chain representing either the antecedent or consequent (see Listing 8.8).

Listing 8.8: The parseElement Function.
start example
 char *parseElement( char *block, memoryElementType **met ) {   memoryElementType *element;   int i=0;   int balance = 1;   element = (memoryElementType *)malloc(sizeof(memoryElementType));   element->element[i++] = *block++;   while (1) {     if (*block == 0) break;     if (*block == ')') balance--;     if (*block == '(') balance++;     element->element[i++] = *block++;     if (balance == 0) break;   }   element->element[i] = 0;   element->next = 0;   if (*met == 0) *met = element;   else {     memoryElementType *chain = *met;     while (chain->next != 0) chain = chain->next;     chain->next = element;   }   return block; } 
end example
 

Finally, the skipWhiteSpace function completes the file parser. This function simply skips any characters that are not active symbols within a rule (such as '(' or '=' ). The function returns the new position of the input file where the caller will begin parsing (see Listing 8.9).

Listing 8.9: The Whitespace Consumer Function.
start example
 char *skipWhiteSpace( char *block ) {   char ch;   while (1) {     ch = *block;     while ((ch != '(') && (ch != ')') && (ch != '=') &&            (ch != 0) && (ch != ';')) {       block++;       ch = *block;     }     if (ch == ';') {       while (*block++ != 0x0a);     } else break;   }   return block; } 
end example
 

Upon completion of the parser, the ruleSet will have been populated with the rules from the file. We continue from Listing 8.5 to the interpret function (see Listing 8.10). This function simply walks through the active rules and calls the checkRule function to see if their antecedents match. The checkRule function returns one if the rule fires, zero otherwise . If the rule fired, we break from the loop and start again from the top of the rules set.

Listing 8.10: The interpret Function.
start example
 void interpret( void ) {   int rule;   int fired = 0;   extern int checkRule( int );   extern int debug;   for (rule = 0 ; rule < MAX_RULES ; rule++) {     fired = 0;     if (ruleSet[rule].active) {       fired = checkRule( rule );       /* If a rule had some effect on working memory, exit,        * otherwise test another rule.        */       if (fired) break;     }   }   if (debug) {     if (fired) printf("Fired rule %s (%d)\n",                        ruleSet[rule].ruleName, rule);   }   return; } 
end example
 

The checkRule function, while notably short and simple, begins the most complex portion of the rules-based system (see Listing 8.11). Function checkRule calls the checkPattern function to try to match the antecedents of the current rule with the fact in working memory.

Listing 8.11: The checkRule Function.
start example
 int checkRule( int rule ) {   int fire = 0;   char arg[MEMORY_ELEMENT_SIZE]={0};   extern int fireRule( int, char * );   fire = checkPattern(rule, arg);   if (fire == 1) {     fire = fireRule( rule, arg );   }   return fire; } 
end example
 

The checkPattern function returns one if the antecedents match. If a match is found, the rule is permitted to fire using the fireRule function. Note that we use the fire variable again for the return value of fireRule , and recall that the return of this routine defines whether the rule in question fired. When we match a rule (via checkPattern ), we allow the matched rule to speculatively fire. If the rule changes working memory, then the rule actually fired and this status is returned. If the rule does not change working memory, we pretend that the rule did not fire and continue our search by returning a zero value. This is one element of conflict resolution, and avoids the problem of a rule firing repeatedly, simply because it appears before other rules whose antecedents match elements in the working memory.

The purpose of the checkPattern function is to match the antecedents of the current rule to facts in working memory. While this is a conceptually simple task, what happens if our rule appears as:

 (defrule check-fully-charged       (fully-charged ?) =>       (trickle-charge ?) ) 

Recall that in this case, the rule will match for the first term of the antecedent (fully-charged), and the second element is used in the consequents. So if our working memory appears as:

 (fully-charged battery-1) 

Then upon firing, the rule will be added to working memory:

 (trickle-charge battery-1) 

Therefore, the first step in rule matching is to check whether the antecedent has a '?' as the second term of the element. If so, then we need to store each of the first terms of these rules to use in subsequent matching of the rules.

The checkPattern function is shown in Listing 8.12.

Listing 8.12: The Rule Matching Algorithm, checkPattern .
start example
 int checkPattern( int rule, char *arg ) { int ret=0; char term1[MEMORY_ELEMENT_SIZE+1]; char term2[MEMORY_ELEMENT_SIZE+1]; memoryElementType *antecedent = ruleSet[rule].antecedent; while (antecedent) {   sscanf( antecedent->element, "(%s %s)", term1, term2);   if (term2[strlen(term2)-1] == ')') term2[strlen(term2)-1] = 0;   if (term2[0] == '?') {     int i;     char wm_term1[MEMORY_ELEMENT_SIZE+1];     char wm_term2[MEMORY_ELEMENT_SIZE+1];     for (i = 0 ; i < MAX_MEMORY_ELEMENTS ; i++) {       if (workingMemory[i].active) {         sscanf( workingMemory[i].element, "(%s %s)",                  wm_term1, wm_term2 );         if (wm_term2[strlen(wm_term2)-1] == ')')           wm_term2[strlen(wm_term2)-1] = 0;         if (!strncmp(term1, wm_term1, strlen(term1))) {           addToChain(wm_term2);         }       }     }   }   antecedent = antecedent->next; } /* Now that we have the replacement strings, walk through  * the rules trying the replacement string when necessary.  */ do {     memoryElementType *curRulePtr, *temp;     curRulePtr = ruleSet[rule].antecedent;     while (curRulePtr) {       sscanf( curRulePtr->element, "(%s %s)", term1, term2 );       if (term2[strlen(term2)-1] == ')') term2[strlen(term2)-1]         = 0;       if (!strncmp( term1, "true", strlen(term1))) {         ret = 1;         break;       } else {         if ((term2[0] == '?') && (chain)) {           strcpy(term2, chain->element);         }       }       ret = searchWorkingMemory( term1, term2 );       if (!ret) break;       curRulePtr = curRulePtr->next;     }     if (ret) {       /* Cleanup the replacement string chain */       while (chain) {         temp = chain;         chain = chain->next;         free(temp);       }       strcpy(arg, term2);     } else {       if (chain) {         temp = chain;         chain = chain->next;         free(temp);       }     }   } while (chain);   return ret; } 
end example
 

The first portion of checkPattern walks through the list of antecedents for the current rule and finds each element that has a second term of '?'. The first term of this element is then searched in the working memory and when found, the second element of the fact is stored via a call to addToChain . This is referred to as the replacement string (the actual value in working memory for the '?' second rule term). When this loop has completed, we have a chain of elements representing the second term of facts in memory that match rules with second terms of '?'. Using this, the second loop walks through the antecedents for the current rule, using each of the stored second terms in the chain linked-list. Once the antecedent has been fully traced with a given chain element, the working memory matched the rule and the stored second term (as defined by the current element in the chain) is copied and returned to the caller. This argument (such as 'battery-1' in our previous example) is used later when working through the consequents of the rule.

The addToChain function, discussed previously, simply builds a chain of terms that are used in pattern matching (see Listing 8.13). The searchWorkingMemory function (see Listing 8.14) attempts to mach the two terms of an antecedent with a fact in working memory. Upon matching, a one is returned to the caller (otherwise zero).

Listing 8.13: Building a Chain of Elements with addToChain .
start example
 void addToChain( char *element ) {   memoryElementType *walker, *newElement;;   newElement = (memoryElementType *)malloc(sizeof(memoryElementType));   strcpy( newElement->element, element );   if (chain == NULL) {     chain = newElement;   } else {    walker = chain;    while (walker->next) walker = walker->next;    walker->next = newElement;   }   newElement->next = NULL; } 
end example
 
Listing 8.14: Matching Antecedents to Facts in Working Memory with searchWorkingMemory .
start example
 int searchWorkingMemory( char *term1, char *term2 ) {   int ret = 0;   int curMem = 0;   char wm_term1[MEMORY_ELEMENT_SIZE+1];   char wm_term2[MEMORY_ELEMENT_SIZE+1];   while (1) {     if (workingMemory[curMem].active) {       /* extract the memory element */       sscanf(workingMemory[curMem].element, "(%s %s)",               wm_term1, wm_term2);       if (wm_term2[strlen(wm_term2)-1] == ')') {         wm_term2[strlen(wm_term2)-1] = 0;       }       if ((!strncmp(term1, wm_term1, strlen(term1))) &&           (!strncmp(term2, wm_term2, strlen(term2)))) {         ret = 1;         break;       }     }     curMem++;     if (curMem == MAX_MEMORY_ELEMENTS) {       break;     }   }   return ret; } 
end example
 

If a rule is matched to working memory, then one is returned from checkPattern to the checkRule function (see Listing 8.11). This indicates that the rule may fire and the fireRule is invoked with the index of the rule to fire and the argument that was stored from checkPattern . Recall that this value is the matched '?' rule term from working memory.

Firing a rule is quite a bit simpler than matching a rule in working memory. To fire a rule, we simply walk through the linked-list of consequents for the rule and perform the command encoded by each element (see Listing 8.15).

Listing 8.15: Firing a Rule with fireRule .
start example
 int fireRule( int rule, const char *arg ) {   int ret;   memoryElementType *walker = ruleSet[rule].consequent;   char newCons[MAX_MEMORY_ELEMENTS+1];   while (walker) {     if (!strncmp(walker->element, "(add", 4)) {       constructElement( newCons, walker->element, arg );       ret = performAddCommand( newCons );     } else if (!strncmp(walker->element, "(delete", 7)) {       constructElement( newCons, walker->element, arg );       ret = performDeleteCommand( newCons );     } else if (!strncmp(walker->element, "(disable", 8)) {       ruleSet[rule].active = 0;       ret = 1;     } else if (!strncmp(walker->element, "(print", 6)) {       ret = performPrintCommand( walker->element );     } else if (!strncmp(walker->element, "(enable", 7)) {       ret = performEnableCommand( walker->element );     } else if (!strncmp(walker->element, "(quit", 5)) {       extern int endRun;       endRun = 1;     }     walker = walker->next;   }   return ret; } 
end example
 

The fireRule function walks through each of the consequents and decodes the command contained in each. A chain of if-then-else statements compares the known command types and calls a special function that handles the execution of the particular command. There are two cases where the working memory is manipulated (add and delete) and therefore, the proper fact must be constructed . Recall that if the antecedent was an inexact match (such as (fast-charge ?) ), then we use the passed argument ( arg ) to construct the actual fact being applied to working memory. This function is shown in Listing 8.16.

Listing 8.16: Constructing a New Fact Using constructElement .
start example
 void constructElement( char *new, const char *old, const char *arg ) {   /* Find the second paren */   old++;   while (*old != '(') old++;   while ((*old != 0) && (*old != '?')) *new++ = *old++;   /* This was a complete rule (i.e., no ? element) */   if (*old == 0) {     *(--new) = 0;     return;   } else {     /* Copy in the arg */     while (*arg != 0) *new++ = *arg++;     if ( *(new-1) != ')') *new++ = ')';     *new = 0;   }   return; } 
end example
 

The constructElement function very simply creates a new fact based upon the pattern fact (consequent) and the passed argument. If the pattern fact contains a '?', then the passed argument replaces the '?' and is returned as the new fact. Therefore, if our argument was "battery-1" and our consequent was "(fast-charge ?)" , then our new fact would be "(fast-charge battery1)" .

Let's now look at the functions that implement the available commands. The first is the add command which adds a new fact to working memory (see Listing 8.17). The add command has the format:

 (add (fast-charge ?)) 
Listing 8.17: Adding a New Fact to Working Memory.
start example
 int performAddCommand( char *mem ) {   int slot;   /* Check to ensure that this element isn't already in    * working memory    */   for (slot = 0 ; slot < MAX_MEMORY_ELEMENTS ; slot++) {     if (workingMemory[slot].active) {       if (!strcmp( workingMemory[slot].element, mem )) {         /* Element is already here, return */         return 0;       }     }   }   /* Add this element to working memory */   slot = findEmptyMemSlot();   if (slot < MAX_MEMORY_ELEMENTS) {     workingMemory[slot].active = 1;     strcpy( workingMemory[slot].element, mem );   } else {     assert(0);   }   return 1; } 
end example
 

for an inexact match, or could represent an absolute fact, such as:

 (add (fast-charge battery-2)) 

The add command first tests to see whether the fact already exists in working memory. If so, it returns a zero to the caller (and error) and exits. Otherwise, an empty slot is found (using findEmptyMemSlot , see Listing 8.18) and the fact is copied into working memory. A one is returned to indicate that working memory was changed. Note that the return status of this function is returned all the way up to ruleFire to support the speculative execution of the rule.

Listing 8.18: Finding an Empty Slot in Working Memory.
start example
 int findEmptyMemSlot( void ) {   int i;   for (i = 0 ; i < MAX_MEMORY_ELEMENTS ; i++) {     if (!workingMemory[i].active) break;   }   return i; } 
end example
 

The delete command removes a fact from working memory (see Listing 8.19). This function simply tries to match the constructed fact with a fact in working memory. If the fact is found in working memory, it is removed (by setting it inactive and zeroing out the string element). The delete command has the format:

 (delete (fast-charge ?)) 
Listing 8.19: Removing a Fact from Working Memory.
start example
 int performDeleteCommand( char *mem ) {   int slot;   int ret = 0;   char term1[MEMORY_ELEMENT_SIZE+1];   char term2[MEMORY_ELEMENT_SIZE+1];   char wm_term1[MEMORY_ELEMENT_SIZE+1];   char wm_term2[MEMORY_ELEMENT_SIZE+1];   sscanf( mem, "(%s %s)", term1, term2 );   for ( slot = 0 ; slot < MAX_MEMORY_ELEMENTS ; slot++ ) {     if ( workingMemory[slot].active ) {       sscanf( workingMemory[slot].element, "(%s %s)",               wm_term1, wm_term2 );       if (!strncmp(term1, wm_term1, strlen(term1)) &&           !strncmp(term2, wm_term2, strlen(term2))) {         workingMemory[slot].active = 0;         bzero( workingMemory[slot].element, MEMORY_ELEMENT_SIZE );         ret = 1;       }     }   }   return ret; } 
end example
 

As with the performAddCommand , the return status of this function is returned all the way up to ruleFire to support the speculative execution of the rule.

The print command simply emits the second term of a fact to standard-out (see Listing 8.20). The second term is a double-quote delimited string. The function ( performPrintCommand ) searches for the first ' " ' and then emits all characters until the terminating ' " ' character is found. Facts for emitting strings have the format:

 (print ("this is a test.")) 
Listing 8.20: Emitting a String.
start example
 int performPrintCommand( char *element ) {   char string[MAX_MEMORY_ELEMENTS+1];   int i=0, j=0;   /* Find initial '"' */   while ( element[i] != '"') i++;   i++;   /* Copy until we reach the end */   while ( element[i] != "") string[j++] = element[i++];   string[j] = 0;   printf("%s\n", string);   return 1; } 
end example
 

The enable command is used to start a timer (see Listing 8.21). A fact for starting a timer has the format:

 (enable (timer N T)) 
Listing 8.21: Enabling a Timer.
start example
 int performEnableCommand( char *element ) {   char *string;   int timer, expiration;   void startTimer( int, int );   string = strstr( element, "timer" );   sscanf( string, "timer %d %d", &timer, &expiration );   startTimer( timer, expiration );   return 1; } 
end example
 

where 'N' is the timer index and 'T' is the expiration time in seconds.

Two final commands are disable and quit . These two commands are handled inline to the fireRule function (see Listing 8.15). The disable command is used to disable a rule from the current rule-set (it will no longer be capable of firing). The quit command ends the run of the rules-based system. The commands have the format:

 (disable (self)) (quit null) 

The final set of functions relate to timer processing. The processing of timers is handled through the processTimers function (shown in Listing 8.22). This function simply decrements the expiration time for the currently active timers until they reach their trigger value (in this case, zero). Once a timer has fired, the fireTimer function is called (see Listing 8.23).

Listing 8.22: Processing the Timer List.
start example
 void processTimers( void ) {   int i;   for (i = 0 ; i < MAX_TIMERS ; i++) {     if (timers[i].active) {       if (--timers[i].expiration == 0) {         fireTimer( i );       }     }   }   return; } 
end example
 
Listing 8.23: Firing a Timer.
start example
 int fireTimer( int timerIndex ) {   int ret;   char element[MEMORY_ELEMENT_SIZE+1];   extern int performAddCommand( char *mem );   sprintf( element, "(timer-triggered %d)", timerIndex );   ret = performAddCommand( element );   timers[timerIndex].active = 0;   return ret; } 
end example
 

Recall that firing a timer involves adding a fact to working memory that represents this, which will act as a trigger for an active rule. When a timer is fired, a fact such as "(timer-triggered X)" is added to working memory (where X represents the timer index). The add function ( performAddCommand ) is used to add the trigger fact to working memory. The assumption here is that another rule in the rule-set will have an antecedent for "(timer-triggered X)" to process the timer event.

Starting a timer is called by the enable command. This function is provided here (see Listing 8.24) to encapsulate all timer functionality into a single file.

Listing 8.24: Starting a Timer.
start example
 void startTimer( int index, int expiration ) {   timers[index].expiration = expiration;   timers[index].active = 1;   return; } 
end example
 



Visual Basic Developer
Visual Basic Developers Guide to ASP and IIS: Build Powerful Server-Side Web Applications with Visual Basic. (Visual Basic Developers Guides)
ISBN: 0782125573
EAN: 2147483647
Year: 1999
Pages: 175

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net