Making decisions: wftk_decide

Previous: Taking external action ] [ Top:  ] [ Next: The interpreter: figuring out what to do next ]

This approach to XML decision trees is the result of a year's thought, more or less. We'll see how it works. It's uniquely appropriate to XML, because the decision process is really a collapsing of an XML element hierarchy. The decision takes place in the context of a datasheet, of course, for access to values.

The problem with representing an if-then-else structure in XML, of course, is that it doesn't fit. The obvious first way to code an "if" would be:
 <if value="3" equal="3">result</if>
but where do you put an "else"? You don't want it in the content of the if, because then you can't tell the difference between actual content and the "else", and it's not elegant to have results of the same test at different levels of the hierarchy -- sooner or later you'd screw it up. And where do you put negations? Where do you put boolean functions like "and" or "or"? Into the tests? Then how many different test attributes will you allow? It just doesn't work and it's driven me crazy until now.

So I think I've got it worked out.

Basically, these elements are involved: decide, if, then, else, any, all, and unless. The rules work like this: we scan elements of the top-level decision element. An "if" is a single test, and if it succeeds, then its attributes are written to the decision element and its content replaces that of the decision element:
<decide>
 <if value="3" equal="3" result="yes">content here</if>
</decide>
thus reduces to <decide result="yes">content here</decide>, while
<decide>
 <if value="3" equal="4" result="yes">content here</if>
<decide>
just reduces to <decide/>.

It's obvious, then, what else should do. It's the catchall that succeeds when all ifs have failed:
<decide>
 <if value="3" equal="2" result="2">it was two</if>
 <if value="3" equal="4" result="4">it was four</if>
 <else result="neither">it wasn't two or four</else>
</decide>
reduces to <decide result="neither">it wasn't two or four</decide>. See? So far, so good. Note that we can have multiple cases, since XML makes it easy to put them there. In this, the decide tag is really more like a select statement or a LISP cond statement.

OK. I'll gloss over the unless element, which simply is a negated if, and cut to the chase with combinations: all requires that all of its tests succeed, and any will succeed if any of its tests succeed. The tests are again if elements, but in this case, we need a catchall place to put results and content, so that's where the then element comes into play. An example might be a good idea:
<decide>
  <if value="3" equal="2" result="2">it was two</if>
  <any anyfired="yep">
    <if value="3" equal="4" result="4"/>
    <if value="3" equal="3" result="3"/>
    <then thenresult="then was here">it was either four or three</then>
  </any>
</decide>
will reduce to <decide anyfired="yep" result="3" thenresult="then was here">it was either four or three</decide>. The all element works in an entirely analogous manner.

Another decide element may replace any then or else for recursive decisions. I don't like that much, but at the moment I don't see a better solution for implementing nested tests, for the same reasons that the if-then-else doesn't work: you can't effectively mix arbitrary content with elements you want to do something with.
 
See Testing for criteria
See Collapsing successful branches into the result
WFTK_EXPORT XML * wftk_decide (void * session, XML * datasheet, XML * decision) {
   XML * elem;
   XML * elem2;
   XML * mark;
   XML * result;
   XML_ATTR * attr;
   int fire = 0;

   if (!decision) return 0;

   result = xml_create ("decide");
   attr = xml_attrfirst (decision);
   while (attr) {
      xml_set (result, xml_attrname (attr), xml_attrvalue (attr));
      attr = xml_attrnext (attr);
   }
   elem = xml_firstelem (decision);
   while (elem) {
      if (xml_is (elem, "if") || xml_is (elem, "unless")) {
         if (_wftk_decide_test (session, datasheet, elem)) {
            _wftk_decide_collapse (result, elem);
            return (result);
         }
      } else if (xml_is (elem, "else")) {
         _wftk_decide_collapse (result, elem);
         return (result);
      } else if (xml_is (elem, "any")) {
         elem2 = xml_firstelem (elem);
         while (elem2) {
            fire = 0;
            if (xml_is (elem2, "if") || xml_is (elem2, "unless")) {
               if (_wftk_decide_test (session, datasheet, elem2)) {
                  _wftk_decide_collapse (result, elem);
                  _wftk_decide_collapse (result, elem2);
                  fire = 1;
               }
            } else if (xml_is (elem2, "then") && fire) {
               _wftk_decide_collapse (result, elem);
               _wftk_decide_collapse (result, elem2);
               return (result);
            }
            elem2 = xml_nextelem (elem2);
         }
         if (fire) {
            _wftk_decide_collapse (result, elem);
            return (result);
         }
      } else if (xml_is (elem, "all")) {
         elem2 = xml_firstelem (elem);
         while (elem2) {
            if (xml_is (elem2, "if") || xml_is (elem2, "unless")) {
               if (!_wftk_decide_test (session, datasheet, elem2)) {
                  break;
               }
            } else if (xml_is (elem2, "then")) {
               _wftk_decide_collapse (result, elem);
               _wftk_decide_collapse (result, elem2);
               return (result);
            }
            elem2 = xml_nextelem (elem2);
         }
         if (!elem2) {
            _wftk_decide_collapse (result, elem);
            return (result);
         }
      } else if (xml_is (elem, "decide")) {
         mark = wftk_decide (session, datasheet, elem);
         _wftk_decide_collapse (result, mark);
         xml_set (result, "loc", xml_attrval (mark, "loc"));
         xml_free (mark);
         return (result);
      }
      elem = xml_nextelem (elem);
   }
   return (result);
}


Testing for criteria
Actually checking values for equality or whatever is done the same for if and unless, so I'm going to split it out into a function. That makes it easier to extend, too.
 
int _wftk_decide_test (void * session, XML * datasheet, XML * element)
{
   int result = 0;
   char * value;
   char * which;
   char * test;

   if (*xml_attrval (element, "equal")) {  /* Cutting corners.  TODO: maybe some more choices? */
      which = "equal";
   } else return result;

   value = wftk_value_interpreta (session, datasheet, xml_attrval (element, "value"));
   test  = wftk_value_interpreta (session, datasheet, xml_attrval (element, which));

   if (!strcmp (which, "equal")) {
      result = !strcmp (value, test);
   }

   free (value);
   free (test);

   if (xml_is (element, "unless")) return !result;
   return result;
}
Some more interesting comparisons would be numeric greater than/less than, glob matching, regexp, and so forth.

Collapsing successful branches into the result
Since this gets repeated all over, it's another function.
 
int _wftk_decide_collapse (XML * result, XML * element)
{
   XML_ATTR * attr;
   XML * mark;

   attr = xml_attrfirst (element);
   while (attr) {
      if ((strcmp (xml_attrname (attr), "value")) ||
          (strcmp (xml_attrname (attr), "equal"))) {
         xml_set (result, xml_attrname (attr), xml_attrvalue (attr));
      }
      attr = xml_attrnext (attr);
   }
   xml_set_nodup (result, "loc", xml_getlocbuf (element));
   mark = xml_first (element);
   while (mark) {
      xml_append (result, xml_copy (mark));
      mark = xml_next (mark);
   }
}
Previous: Taking external action ] [ Top:  ] [ Next: The interpreter: figuring out what to do next ]


This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2000, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license.