Bob Marinier : foreach

Introduction

Sometimes you need to do something some number of times that won't be knowable until runtime, and even then might not be knowable directly. For example:

  • Doing something to every element of a collection (e.g., a mapping operation)
    • Approach 1 and 2 work
  • Doing something for every element of a collection (e.g., counting)
    • May require approach 1 if side effects build

Approach 1: Goal with a terminating operator

Note this works for both NGS and Michigan approaches.

  • Create a goal/subgoal for the processing
  • Propose an operator with worst preference to terminate the goal (e.g., by setting a flag)

All other operators will get a chance to do their thing, and then when there's nothing left to do, the goal will terminate. Note this takes no stance on how many operators or what their nature is.

sp "my-goal*propose*finish
   (state <s> ^name my-goal)
-->
   (<s> ^operator <o> + <)
   (<o> ^name finish)
"

sp "my-goal*propose*finish*ngs
   [NGS_match-active-goal my-goal <g> <s>]
-->
   [NGS_create-operator finish <g> <o> "+ = <"]
"

For example, suppose we wanted to put the "^processing true" flag on all elements in the objects collection:

sp "propose*mark-all-objects
   (state <s> ^objects <objs>
             -^processed true)
-->
   (<s> ^operator <o> +)
   (<o> ^name mark-all-objects)
"

# this operator will propose/apply once for each object
sp "mark-all-objects*propose*mark-processed
   (state <s> ^name mark-all-objects
              ^args.objects.object <obj>)
  -(<obj> ^processing true)
-->
   (<s> ^operator <o> + =)
   (<o> ^name mark-processed 
        ^object <obj>)
"

sp "mark-all-objects*apply*mark-processed 
   (state <s> ^name mark-all-objects
              ^operator <o>)
   (<o> ^name mark-processed 
        ^object <obj>)
-->
   (<obj> ^processed true)
"

# won't get selected until all objects are processed, then returns a result to terminate the goal
sp "mark-all-objects*propose*finish
   (state <s> ^name mark-all-objects)
-->
   (<s> ^operator <o> + <)
   (<o> ^name finish)
"

sp "mark-all-objects*apply*finish
   (state <s> ^name mark-all-objects
              ^operator.name finish
              ^superstate <ss>)
-->
   (<ss> ^processed true)
"

What happens is the goal is proposed, and then the finish operator is immediately proposed – but so are operators for every object. The object operators will all get selected first (one at a time), and when they have all done their thing, the finish operator will finally get selected and return a result that terminates the goal.

It should be noted that, in this simple example, it would have been easy to have the goal proposal rule test that there were no objects that weren't marked (see Approach 2 below for how to do this), thus obviating the need for a terminating operator. When avoiding a terminating operator is easy and clear, it is probably preferred. But sometimes things are more complicated and a terminating operator is the best approach (where best is defined by ease of writing, understanding, and maintaining).

It should also be noted that there's no reason the processing operator couldn't have kicked off another subgoal to do a substantial amount of work on each object.

Pros:

  • Supports reactive code (i.e., doesn't tie up decision cycle with potentially long phases)
  • Supports complex processing (i.e., subgoals)

Cons:

  • (NGS) Using a worst pref means the goal won't terminate until all other (non-worst) operators from all other goals have terminated. In the worst case, this means it will never terminate. Alternatives include:
    • Make terminator worse than all operators associated with that goal. If there are a lot, this can generate a large number of preferences which could slow down the decision cycle.
    • Condition terminator proposal on there being no other proposed operators for this goal.
sp "my-goal*select*finish*worse-than-other-goal-operators
   [NGS_match-active-goal my-goal <g> <s>]
   [NGS_match-operator finish <o1> <g>]
   [NGS_match-operator finish <o2> <g>]
-->
   (<s> ^operator <o1> < <o2>)
"

sp "my-goal*propose*finish*no-other-goal-operators-proposed
   [NGS_match-active-goal my-goal <g> <s>]
  -{
    (<s> ^operator <o> +)
    (<o> ^goal <g>
        -^name my-goal)
   }
-->
   [NGS_create-operator finish <g> <o>]
"

Approach 2: One operator, multiple applies

In this approach, we piggyback many applies on a single operator.

  • Propose operator to kick off processing
  • Apply rules fire as many times as desired

Here's the same example from above, but with a single operator:

# note: using this elaboration to create a single flag. If we incorporated these conditions into the operator proposal, it would get proposed a lot. This might be ok, but it's inefficient.
sp "elaborate*objects-not-processed 
   (state <s> ^objects.object <obj>)
  -(<obj> ^processing true)
-->
   (<s> ^not-processed true)
"

sp "propose*mark-processed 
   (state <s> ^objects <objs>
              ^not-processed true)
-->
   (<s> ^operator <o> +)
   (<o> ^name mark-processed 
        ^objects <obj>)
"

# this will fire many times in parallel, once for each object
sp "apply*mark-processed 
   (state <s> ^operator <o>)
   (<o> ^name mark-processed 
        ^objects.object <obj>)
-->
   (<obj> ^processed true)
"  

Note that there will be a single wave of applies, during which all objects are marked in parallel. Once they are marked, the elaboration will retract and the operator proposal will retract.

Pros:

  • Avoids overhead of decision cycle by doing everything in parallel at once (although overhead should be pretty minimal)
  • Ensures that the processing happens "atomically" (i.e., there is no state where some objects are marked and some aren't). This could be important in some cases, although if it is you might reconsider your design as it's pretty brittle.

Cons:

  • Supporting multiple waves of rule firings is difficult. In the example, everything must occur in a single wave (the operator will retract after that, so there will be no second wave). Of course, the proposal could be conditioned on something that won't happen until the nth wave of rule firings, but in complex systems, getting the multiple waves to happen the way you want is often difficult.
  • Reduces reactivity: The agent is stuck in this decision until all the applies have fired – it can't get new input or work on anything else in the meantime. If the number of objects is small, this is not a big deal. But if there are 1000 objects, this could matter.
    • It should be noted that in some cases this could be advantageous – e.g., it gives an opportunity to do something to every object on the input-link before they potentially disappear in the next cycle. But this seems like a brittle design – there may be better ways to deal with disappearing input.
  • If the processing has side effects, doing them all in parallel may not work. E.g., counting is an inherently sequential process, and thus Approach 1 is necessary.
  • Doesn't support complex (i.e., subgoal) processing of each object