Bob Marinier : generic processes

Introduction

Reusable code is a hallmark of good design. Standard programming languages will package reusable code as functions, classes, etc. Reusable code is also the basis of libraries, which are critical to doing anything complex in a short amount of time.

There are multiple ways to create reusable code. Here we will discuss reusable operators and subgoals. (Another major way to create reusable code is Tcl macros, discussed elsewhere.) The general approach applies in both Michigan style and NGS code. Reusable operators/subgoals are often called "floating" operators/subgoals, as they can be inserted anywhere in the goal hierarchy.

Related sections include: aspectscallbacks

Approach: Specific proposals, generic applies

The basic approach is to have a common apply rule (or subgoal), and to have context-specific rules propose the operator or create the goal wherever desired.

For example, suppose we wanted to send a message using Aria (né simradio). We could set up a generic apply rule like this:

sp "apply*send-message
   (state <s> ^operator <o>
              ^top-state.io.output-link <ol>)
   (<o> ^name send-message
        ^radio-id <rid>
        ^message <msg>)
-->
   (<ol> ^radio-command <rc>)
   (<rc> ^radio-id <rid>
         ^transmit <msg>)
"

Then, in all the places you want to send a radio message, you simply propose the operator (executable example here):

sp "my-goal*propose*send-message
   (state <s> ^name my-goal)
   # any other context
-->
   (<s> ^operator <o> +)
   (<o> ^name send-message
        ^radio-id my-radio
        ^message |Hello world!|)
"

Of course, the proposal could just as well have tested an NGS goal, or no goal at all. The key here is that the apply doesn't test a goal, and thus can apply on any goal (in some sense, this is intentionally doing the thing I said to avoid in General guidance, but here it has a purpose).

This is a nice way of hiding the details of how sending a message actually works – if the format of the output command changed, you would only have to update one place – indeed, this is the point of code reuse.

Notice also how we are treating operators like function calls as discussed in General guidance.

What about a more complex situation, like an entire subgoal? The same basic approach applies: the entire subgoal is encoded generically, and it is created in any of the places that want to use it.

Let's consider a more complex version of send-message. This example is shown using NGS, but it's easily translated to Michigan style.

In this version, the subgoal does two things: it sends the message, and then it returns a result defined by the caller. The send-message operator also supports an optional priority argument (after all, reusable code should strive to be generally useful), so some of the rules are that in. (For a more complete discussion of optional arguments and default values, see default values). It also uses Tcl macros for some common constants, such as the input-link, output-link, and the radio-id.

#
# achieve-send-message/send-message.soar
#

# Once we are in the achieve-send-message subgoal, propose the send message operator
# Then elaborate on the priority (either as provided or a default)
# Finally apply the operator

sp "achieve-send-message*propose*send-message
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_is-not-tagged <g> message-sent true]
   [NGS_is-tagged <g> message <msg>]
-->
   [NGS_create-operator send-message <g> <o>]
   (<o> ^message <msg>)
"

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)
"

sp "achieve-send-message*apply*send-message
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_match-operator send-message <o> <g>]
   (<g> ^tags <gtags>)
   (<o> ^message <msg>
        ^priority <pri>)
   (<s> ^$olink <ol>)
-->
   (<ol> ^radio-command <rc>)
   (<rc> ^radio-id $radio1
         ^transmit <msg>
         ^priority <pri>)
   (log $info |Sending radio message: |<msg>)
   [NGS_tag <gtags> message-sent true]
"

#
# achieve-send-message/return-result.soar
#

# this creates whatever result the caller asked for
# the intention is that this triggers the retraction of this goal
sp "achieve-send-message*propose*return-result
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_is-tagged <g> message-sent true]
   [NGS_is-tagged <g> result-id <id>]
   [NGS_is-tagged <g> result-attr <attr>]
   [NGS_is-tagged <g> result-val <val>]
-->
   [NGS_create-operator return-result <g> <o>]
   (<o> ^result-id <id>
        ^result-attr <attr>
        ^result-val <val>)
"
sp "achieve-send-message*apply*return-result
   [NGS_match-active-goal achieve-send-message <g> <s>]
   [NGS_match-operator return-result <o> <g>]
   (<o> ^result-id <id>
        ^result-attr <attr>
        ^result-val <val>)
-->
   (<id> ^<attr> <val>)
"

To send a message using this subgoal, you would create the subgoal wherever you needed it like this (executable example here):

sp "my-goal*create-subgoal*achieve-send-message
   [NGS_match-active-goal my-goal <g> <s>]
   [NGS_match-goalpool <goals>]
  -(<s> ^message-sent true)
   # any other context
-->
   [NGS_create-subgoal <goals> achieve-send-message <g> <sg> <sgtags>]
   [NGS_tag <sgtags> message "|Hello world!|"]
   [NGS_tag <sgtags> result-id <s>]
   [NGS_tag <sgtags> result-attr message-sent]
   [NGS_tag <sgtags> result-val true]
"

All of the arguments needed to execute the subgoal are attached to the goal creation (or, in Michigan style, to the operator proposal that triggers the ONC impasse). In this example, the optional priority is left out, but adding it is simply a matter of putting on a "priority" tag. When the goal executes, it will send the message and then create the specified result – in this case (<s> ^message-sent true). The creation of this result triggers the retraction of the subgoal.

Generally speaking, you will need some way to know that the subgoal has completed. The subgoal could return a fixed result, but this has several downsides: it essentially makes a particular attribute/value "reserved", and it imposes a restriction on use, namely where the result should go. (I have had situations where the place to put the result was actually up several levels in the goal hierarchy, resulting in an entire set of subgoals getting retracted at once.) Thus, it is more flexible, and not much more work, to allow the caller to specify where the result should go.

As discussed in callbacks, it is also possible to specify considerably more than just where a result should go.

Attachments:

generic-processes-floating-subgoal.soar (application/octet-stream)
generic-processes-floating-operator.soar (application/octet-stream)