Bob Marinier : callbacks

Introduction

The section on generic processes discussed how to create reusable subgoals. There, we talked about how to allow callers to specify where a result should go. Here we take that to a whole new level and allow callers to specify what operators to propose in the subgoal. In other words, how to allow callers to define callbacks.

As usual, everything here applies to both Michigan style and NGS, although the particular example I'll use is Michigan style.

Approach: Name the operators to propose in the goal arguments

The approach is really simple: implement the apply for the operator you want to be called by the subgoal, and then pass the name of it to the subgoal when its created. The subgoal will propose an operator with that name, and then your apply will fire. Of course, there's no reason why the callback couldn't be an entire other subgoal.

This example is of a subgoal that performs a semantic memory retrieval and then calls one of two different callbacks depending on whether the retrieval succeeds or fails. Each callback also has optional user data that is sent to the callback when it is triggered. (Note, this is a simplified version of an actual subgoal I use for this – the difference is the full subgoal also supports fully expanding the retrieved LTI.)

# retrieve-smem/elaborations.soar

##!
# @brief If the caller did not supply user-data, set to default value nil
# @type state-elaboration
sp "retrieve-smem*elaborate*user-data-nil
   (state <s> ^name retrieve-smem
              ^args.<< on-success on-failure >> <osf>)
  -(<osf> ^user-data <> nil)
-->
   (<osf> ^user-data nil)
"

##!
# @brief If there is an smem failure, flag that. This allows multiple failure results to be mapped to one flag.
# @type state-elaboration
sp "retrieve-smem*elaborate*failure
   (state <s> ^name retrieve-smem
              ^smem.result.failure)
-->
   (<s> ^retrieval-failed true)
"

#
# retrieve-smem/execute-command.soar
#

##!
# @brief
# @type proposal
sp "retrieve-smem*propose*execute-command
   (state <s> ^name retrieve-smem
              ^smem.command <cmd>
              ^args.command <argcmd>)
  -(<cmd> ^<any>)
-->
   (<s> ^operator <o> +)
   (<o> ^name execute-command
        ^command <argcmd>)
"

##!
# @brief This could apply multiple times, e.g. if there is both a query and a neg-query
# @type apply
sp "apply*execute-command
   (state <s> ^operator <o>
              ^smem.command <cmd>)
   (<o> ^name execute-command
        ^command <argcmd>)
   (<argcmd> ^<att> <val>)
-->
   (<cmd> ^<att> <val>)
"

#
# retrieve-smem/on-failure.soar
#

##!
# @brief
# @type proposal
sp "retrieve-smem*propose*on-failure
   (state <s> ^name retrieve-smem
              ^retrieval-failed true
              ^args.on-failure <of>)
   (<of> ^operator-name <op-name>
         ^user-data <data>)
-->
   (<s> ^operator <o> +)
   (<o> ^name <op-name>
        ^result nil
        ^user-data <data>)
"

# apply supplied by caller

#
# retrieve-smem/on-success.soar
#

##!
# @brief
# @type proposal
sp "retrieve-smem*propose*on-success
   (state <s> ^name retrieve-smem
              ^smem.result.retrieved <result>
              ^args.on-success <os>)
   (<os> ^operator-name <op-name>
         ^user-data <data>)
-->
   (<s> ^operator <o> +)
   (<o> ^name <op-name>
        ^result <result>
        ^user-data <data>)
"

# apply rule supplied by caller

With floating subgoals like this, it is generally advisable to document how to use them somewhere. For example:

#
# retrieve-smem.soar
#

# this is a floating operator
# to trigger, propose the retrieve-smem operator
# 
# ^command: the smem command to use
# ^on-success:
#   ^operator-name: name of the operator to propose if retrieval succeeds (after expansion complete, if applicable)
#                   the operator will be passed a ^result pointing to the retrieval and the user-data structure
#   ^user-data: a structure to pass to the proposed operator
# ^on-failure:
#   ^operator-name: name of the operator to propose if retrieval fails
#                   the operator will be passed ^result nil and the user-data structure
#   ^user-data: a structure to pass to the proposed operator
#
# this creates a substate that queries smem using the given command and then proposes user-defined operators depending on whether the retrieval succeeded or not.

As described by the documentation, then, you could call this subgoal like this (note, you have to define the callbacks, too):

sp "apply*my-failure-callback
   (state <s> ^operator <o>)
   (<o> ^name my-failure-callback
        ^user-data.id <id>)
-->
   (<id> ^retrieved nil)
"

sp "apply*my-success-callback
   (state <s> ^operator <o>)
   (<o> ^name my-success-callback
        ^result <result>
        ^user-data.id <id>)
-->
   (<id> ^retrieved <result>)
"

sp "my-goal*propose*retrieve-smem
   (state <s> ^name my-goal
             -^retrieved)
-->
   (<s> ^operator <o> +)
   (<o> ^name retrieve-smem
        ^command <cmd>
        ^on-success <os>
        ^on-failure <of>)
   (<cmd> ^query.type foo
          ^neg-query.value bar)
   (<os> ^operator-name my-success-callback
         ^user-data.id <s>)
   (<of> ^operator-name my-failure-callback
         ^user-data.id <s>)
"

In this example, an smem command with both a query and neg-query is given, with two different callbacks used (in general, there's no reason the same callback couldn't be used for both). The callbacks in this case save the result to a specified location (nil on failure).

For simple usage like this, callbacks may be overkill. But in actual practice, I have taken advantage of the flexibility of callbacks to do more complex things than just save a result. For example, setting other flags, logging a message, etc. Using callbacks allows the core subgoal to remain simple – the alternative would be to try to build in optional support for anything someone might ever want to do. This quickly becomes cumbersome and results in code that is difficult to understand and maintain. Callbacks keep that complexity in the caller-specific code.