3.2 Writing Code for a Series of Tests

Dangers of a Series of Tests

When writing Aspen SCM Expert System code one often wishes to perform a series of tests and quit after the first failure. This leads to code such as
IF      <preparation 1>
AND     <test 1>

AND     <preparation 2>
AND     <test 2>

AND     <preparation 3>
AND     <test 3>

AND     <action>

THEN    <predicate >

Although this is attractive, it is dangerous. The rule is designed to return FALSE if any test fails, so the calling rule has to be coded properly to handle this. More seriously, if a test does fail, the interpreter will backtrack through the preparation code before it returns FALSE to the calling rule. This is fine so long as the preparation code does not contain any ORs. But if the code does contain ORs (e.g. it has error messages after subsidiary tests) or if any duplicate predicates are asserted (see section 2.a) the result will be disastrous. Even if you write the code today without any ORs you are leaving a trap for your successors who may quite innocently seek to “improve” your code by putting in better error-handling and promptly induce backtracking.

Including OR Clauses for each Test

It is better to have the rule always end TRUE and use a return argument to record the status of the tests. This can be done as follows.

IF      <preparation 1>
AND     ( <test 1>

              AND <preparation 2>
              AND ( <test 2>

                        AND <preparation 3>
                        AND ( <test 3>

                                  AND <action>
                                  AND ?FLAG = TRUE

                               OR ?FLAG = FALSE
                             )

                     OR ?FLAG = FALSE
                   )

           OR ?FLAG = FALSE
         )

THEN    <predicate >

The separation of the OR clause from its test is unpleasant, especially if there are many tests so that the separation becomes large. As a rule an OR clause should not be separated from its test by more than the number of lines on a screen, say 60.

Inverting the Tests

It is neater to invert the tests (indicated by a prime after the test) and put the code into a nest of OR clauses. For instance, if test 1 is ?X GT 5 test 1' is ?X LE 5) :

IF      ?FLAG = FALSE

AND     <preparation 1>

AND     ( <test 1'>

           OR <preparation 2>
              AND ( <test 2'>

                     OR <preparation 3>
                        AND ( <test 3'>

                               OR <action>
                                  AND ?FLAG = TRUE
                             )
                   )
         )

THEN    <predicate >

In this case it is only the closing brackets which are separated from the tests by many lines, so this code is easier to follow.

Doing all the Preparation before a Compound Test

If the preparation code executes quickly, there may be negligible run-time benefit from only doing each bit of preparation if it is needed. There is a large benefit from writing code which is immediately comprehensible to your successors (and to you, if you are still around). It is much easier to understand a chunk of preparation followed by a compound test:

nesting and wide separation can be avoided by ANDing the tests together into a single compound test and doing all the preparation in advance:

IF      <preparation 1>
AND     <preparation 2>
AND     <preparation 3>

AND     ( ( <test 1> AND <test2> AND <test3> )

              AND <action>
              AND ?FLAG = TRUE
           OR ?FLAG = FALSE
         )

THEN    <predicate >

Tests within a Loop

Within a loop, if a statement resolves FALSE, the interpreter backtracks to the start of the loop and tries the next element of the set (for an IN loop) or increments the loop counter (for a WHILE loop). It then starts forward again, executing the code with the new value of the loop variable.

The same concerns exist about backtracking as before: if there is a test which fails and the interpreter backtracks through code which contains OR clauses, these will be executed before the loop variable is updated again. But there are two possibilities which ease  matters:

  • it is often useful to do a test early in a loop before any OR clauses;
  • you can use BREAK to quit the loop.

Often you may want to do a loop over those members of a set which satisfy some condition. In that case you can have the test within the loop without writing an OR clause for it. If the test fails, the interpreter will backtrack to the loop statement, update the loop variable and proceed.

IF      ?LOOPVAR IN LOOPSET
AND          <test involving ?LOOPVAR>
AND          <action>

THEN    <predicate>

If you want to stop a loop once a test is satisfied, you can use a BREAK statement. As this causes the interpreter to stop the loop immediately, there is no possibility of backtracking through intermediate code which may contain unexplored OR clauses:

IF     ?LOOPVAR IN LOOPSET

AND        <preparation 1>

AND        ( <test 1> OR BREAK ?LOOPVAR )

AND        <preparation 2>
AND        ( <test 2> OR BREAK ?LOOPVAR )

AND        <preparation 3>
AND        ( <test 3> OR BREAK ?LOOPVAR )

AND        <action>

THEN       <predicate >

Note, however, that this code is somewhat unrealistic: if one of the test fails, you are unlikely to want to quit the loop; instead you probably want to try the next element of the set, i.e. use a NEXT ?LOOPVAR statement. Unfortunately, this doesn't exist in the Aspen SCM Expert System language.

Another disadvantage, both of using naked tests in loops and using BREAK statements, is that there is the possibility that the loop will end without the predicate ever having been reached. This means that the rule ends UNKNOWN and the calling rule needs to be able to handle this (see section 2.b).

Using BREAK within a Dummy Loop

Although BREAK statements have their limitations within genuine loops, you can manufacture a NEXT statement by putting the tests within a dummy loop over a single-element set (here it is called SETOF1) and BREAKing out of it:
IF     ?LOOPVAR IN LOOPSET
AND        ?PASS IN SETOF1

AND            <preparation 1>

AND            ( <test 1> OR BREAK ?PASS )

AND            <preparation 2>
AND            ( <test 2> OR BREAK ?PASS )

AND            <preparation 3>
AND            ( <test 3> OR BREAK ?PASS )

AND            <action>

THEN       <predicate >

If any test fails, the BREAK causes the loop over ?PASS to be finished early. The interpreter then jumps to the start of the outer loop and sets ?LOOPVAR to the value of the next element of LOOPSET. If all the tests succeed, the interpreter proceeds through <action> down to the predicate. The loop over ?PASS has now been completed so it then moves ?LOOPVAR onto the next element of LOOPSET.

As with any loop, there remains the possibility that the rule ends UNKNOWN if the predicate is never reached.


Back                                Next
Comments