Bob Marinier : default values

Introduction

When you're creating generic processes, you may want to support optional arguments. Thus, you need to handle the case where the argument is not supplied. Or you may have an input value that is not always provided. You may even have cases where you want to create default values for your own processing (e.g., a process may create a ^complete true flag, and you want to have a ^complete false flag there until the process completes). You could have special-case handling for when the value is missing, but I generally find that the easiest way to do this is to assume a default value. That way, the rest of the code can work exactly the same (i.e., assume that the value is given). (Of course, in the input case, if you control the input you can change it in the I/O layer. But sometimes you don't control it, or don't want to touch that code.)

In any case, you will probably want to use i-support, so that if a non-default value is created, the default automatically retracts. So (almost) all our examples here will all use i-support. There may be special cases where o-support is desired; the last example below is a case that results in o-support (although as I say there, I wouldn't normally do it that way).

How not to do it

We'll start with a default value on a state, because it's the simplest. Novice Soar programmers will often make a mistake like this (executable example here):

# this will not work
sp "my-goal*elaborate*default-value*0
   (state <s> ^name my-goal
             -^value)
-->
   (<s> ^value 0)
"

This will not work because this rule is i-supported, which means it is truth-maintained, which means it will cause itself to retract. Thus, in a single phase, this rule will fire and retract over and over. Luckily, Soar has a fail-safe to prevent it from getting into an infinite loop. By default, if Soar does 50 elaboration subphases in a row, it will print a warning ("Warning: reached max-elaborations; proceeding to <next phase> phase." with some phase filled in) and move on to the next phase. This is commonly referred to as "hitting max elaborations".

Note that elaborating operators in this way has exactly the same problem, even for selected operators (since operator elaborations are i-supported) (executable examples here and here, respectively):

# this will not work
sp "my-operator*elaborate*default-value*0
   (state <s> ^operator <o> +)
   (<o> ^name my-operator
       -^value)
-->
   (<o> ^value 0)
"

# this also will not work
sp "my-operator*elaborate*default-value*0
   (state <s> ^operator <o>)
   (<o> ^my-operator
       -^value)
-->
   (<o> ^value 0)
"

Right ways to create default values

The key to fixing these is to test something that won't be changed by the rule firing. This could be a specific value, or it could mean creating the default in another location.

State elaboration

For a flag with a small number of possible values, we can just test that none of the other values exist (executable example here):

sp "my-goal*elaborate*default-complete*false
   (state <s> ^name my-goal
             -^complete true)
-->
   (<s> ^complete false)
"

Here's a 3-state flag example (executable example here):

sp "my-goal*elaborate*default-direction*straight
   (state <s> ^name my-goal
             -^direction << left right >>)
-->
   (<s> ^direction straight)
"

For numeric cases, you can simply use greater/less than tests (executable example here):

sp "my-goal*elaborate*default-value*0
   (state <s> ^name my-goal
             -^value > 0)
-->
   (<s> ^value 0)
"

There's also a universal way to do this that subsumes all of the approaches above (executable example here):

sp "my-goal*elaborate*default-value*0
   (state <s> ^name my-goal
             -^value <> 0)
-->
   (<s> ^value 0)
"

Essentially, check that there is no non-default value present. Creating the default doesn't cause this to unmatch. This will work for any type with any number of possible values (finite or infinite). The potential downside is the double-negative can be hard to understand if you're not used to this pattern. So if you do this, you should probably comment it.

Note that if the default value rule fires, and then a non-default value is created, there will be a single subphase in which both the default and non-default value exist (as the default rule won't retract until the next subphase). The working executable examples above all contain rules that print when this happens to make it clear. Similarly, if the system is using a non-default value which goes away, there will be a single subphase in which there is no value (as the default rule won't fire until the next subphase). This is generally not a problem, but it does mean you won't be able to create an assert rule to blindly check that your flag always has exactly one value (instead, you'll have to condition that rule to check at some later point when you know for sure there shouldn't be exactly one value).

Goals

For goals, the typical approach is to elaborate the value (given or default) to a location separate from the goal. For example, onto the operators that care about that value, or onto the goal's state. This requires two rules (one for the default, and one for the given value).

Here's an example using a UM style goal:

sp "my-goal*elaborate*value
   (state <s> ^name my-goal
              ^args.value <val>)
-->
   (<s> ^value <val>)
"

sp "my-goal*elaborate*default-value*0
   (state <s> ^name my-goal
             -^args.value)
-->
   (<s> ^value 0)
"

Here's an example using NGS (note the use of a Tcl variable to provide the default value; that is not specific to NGS) and elaborating the value directly onto an operator using it:

sp "achieve-send-message*elaborate*send-message*priority
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_match-proposed-operator send-message <o>]
   [NGS_is-tagged <g> priority <pri>]
-->
   (<o> ^priority <pri>)
"

sp "achieve-send-message*elaborate*send-message*default-priority
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_match-proposed-operator send-message <o>]
   [NGS_is-not-tagged <g> priority]
-->
   (<o> ^priority $messageDefaultPriority)
"

Operators (that are not goals)

If an operator is generating a goal (as in UM style goals), then I would recommend the above goal approaches. But if an operator is just a simple operator, then there are at least two possibilities: create a value elsewhere (like with goals), or to have multiple apply rules to cover each case.

Here's an example creating a separate value on the operator that is actually used. All the same caveats noted above for goals apply. Note that you can do this on either the proposed or selected operator. It probably doesn't make much difference, but note that if you have lots of proposed operators, only one of which will get selected, it would be inefficient to create all those default values on the proposed operators that will ultimately not be used. So I generally prefer to elaborate on the selected operator, as then we only do the work if we're actually going to apply the operator. Note that all i-supported rules are guaranteed to fire before any o-supported rules, so this is safe.

sp "my-operator*elaborate*value
   (state <s> ^operator <o>)
   (<o> ^my-operator
        ^value <val>)
-->
   (<o> ^internal-value <val>)
"

sp "my-operator*elaborate*default-value*0
   (state <s> ^operator <o>)
   (<o> ^my-operator
       -^value)
-->
   (<o> ^internal-value 0)
"

Another way to do this is to make additional apply rules, one to handle the default case and one for the non-default case. If the rules only need to test a single optional value, then this is pretty much equivalent to the above. But if you have more than two optional values, then you may end up in a situation where you need separate rules to test for every combination of optional values, and that will require far more rules than the above approach. It is also often the case that apply rules are testing other non-trivial things and have non-trivial actions, and having multiple copies of that can open the door for maintenance issues (e.g., where one rule gets updated but not the other, or a typo in one but not the other). Thus I generally prefer to use the above approach. But here's the example using multiple apply rules in case you need it:

sp "apply*my-operator
   (state <s> ^operator <o>)
   (<o> ^name my-operator
        ^value <val>)
-->
   (<s> ^my-value <val>)
"

sp "apply*my-operator*default
   (state <s> ^operator <o>)
   (<o> ^name my-operator
       -^value)
-->
   (<s> ^my-value 0)
"

 

 

Attachments:

defaut-values-bad-state-elaboration.soar (application/octet-stream)
defaut-values-bad-proposed-operator-elaboration.soar (application/octet-stream)
defaut-values-bad-selected-operator-elaboration.soar (application/octet-stream)
defaut-values-state-elaboration1.soar (application/octet-stream)
defaut-values-state-elaboration2.soar (application/octet-stream)
defaut-values-state-elaboration3.soar (application/octet-stream)
defaut-values-state-elaboration4.soar (application/octet-stream)