Introduction
This is a collection of topics that have to do with general Soar style. It is not specific to a particular goal method.
Goals vs. Tasks
It is important for Soar programmers to distinguish between goals and tasks. Doing so avoids sloppy and inflexible code.
Conceptually, goals are things that you want to achieve or maintain, while tasks are the things you do to accomplish goals. For example, suppose you want to face left of your current direction. You could create a goal to turn left, but this is really overly specific – the goal is not to turn left, the goal is to face left. By making this distinction, you make it easy to support multiple ways of accomplishing the goal. Turning left is one option – but so is turning right (although you'll have to turn further of course). Maybe you can get someone to pick you up and turn you (in either direction). These are all specific tasks that can be employed to accomplish the goal.
Of course, goals can have subgoals that embody specific steps in a particular approach to the goal. For example, if I'm going to get someone to turn me, I may first need to find someone. This can be accomplished in a number of ways (calling out vocally, sending an email, waiting until someone walks by, etc.). Since we now support multiple ways of accomplishing getting someone to turn us, I would consider that to be a subgoal. To me, that's the critical distinction: if there are multiple ways of doing something, the something is a goal. Generally speaking, most tasks can be converted to goals. It really comes down to how the conditions are encoded. If the conditions testing for a desired state, then it's probably a goal. If the conditions are just testing that a goal is active, then it's probably a task.
There are at least two major kinds of goals: achievement goals and maintenance goals. Achievement goals are like the turning example above: you want to engage some process until a desired state is achieved, and then the goal is complete. Maintenance goals are about maintaining a current state. For example, an aircraft may have the goal of maintaining its current altitude. Unlike achievement goals, this goal does not go away when the desired state is achieved.
Some people prefer to formalize the goal type in the name of the goal: for example, instead of calling the goal face-left, they would name it achieve-face-left. This helps to clarify that this is in fact a goal (as opposed to a task), and also helps distinguish it from maintain-face-left (which would be a different goal, but without the goal type is indistinguishable from achieve-face-left). Indeed, if you aren't sure if something is a goal or a task, consider whether it makes sense to name it with achieve or maintain at the start of its name.
File Names and Directory Structures
Soar does not care where you put your code. You could put all your rules in one file, or each rule in its own file. The only time ordering matters is that Tcl code needs to be defined before it is used, and rare commands like multi-attributes.
That said, there are sensible ways to structure code. My personal approach is to organize around operators and goals, so that the directory hierarchy mirrors the goal hierarchy. Each operator gets its own file named after the operator (containing the proposals, applies, operator elaborations, and any monitor rules), and each goal gets its own directory, named after the goal (containing all operator files for that goal, as well as any state elaborations for that goal). Shared operators/goals can go in a separate directory named something like "floating-operators". Impasses that are not ONCs get directories named after the impasse. The rules in each directory are generally loaded by a load.soar file or other generic file (Soar IDE and Soar Editor create these for you using their preferred naming conventions). load.soar also loads the load.soar files in its immediate child directories. Thus, loading the top-level load.soar will load all the rules.
Indeed, VisualSoar requires this style, and Soar Editor and Soar IDE support it but let you do anything.
For example:
/my-agent load.soar # source commands for other files in this directory and the load.soar's of direct children elaborations.soar # general rules, like those in the Soar Utility code initialize.soar # the initialize operator propose/apply do-cool-thing.soar # the rule(s) that creates the goal /do-cool-thing # the do-cool-thing goal load.soar # source commands for other files in this directory and the load.soar's of direct children elaborations.soar # elaborations specific to the do-cool-thing goal something.soar # the something operator propose/apply retrieve-smem.soar # the proposal for the shared retrieve-smem goal finish.soar # the finish operator propose/apply /shared retrieve-smem.soar # documentation on how to use retrieve-smem, but no rules /retrieve-smem # the retrieve-smem goal load.soar # source commands for other files in this directory and the load.soar's of direct children elaborations.soar # elaborations specific to the retrieve-smem goal execute-command.soar # the execute-command operator propose/apply # etc.
You don't have to strictly follow this convention (or at all). The point is simply to have some reasonable organization to the code. Doing it in this way makes it easy to find rules when you're debugging, and to get a sense of how the program works just from looking at the directory structure.
That said, there is at least one drawback: it is easy to create deep structures that ultimately exceed the path length limit of your OS (I use windows which has a limit of something like 260 characters, and this happens to me occasionally). You probably won't notice right away – you can create the structures as deep as you want, and they will load in Soar no problem. But if you try to copy your agent elsewhere, the copy may fail partway, claiming it can't handle some file because the path is too long. You could restructure your agent to avoid this, but it's also very reasonable to just abbreviate some of your directory names (so they might not exactly match the associated goals, but it'll be close enough to avoid confusion).
Test Goal Name in Apply Rules
This is not a hard-and-fast rule, as there may be special cases where you don't want to do this. But it is a good rule of thumb: test the name of the goal in apply rules. In a complex agent with lots of goals and subgoals, you will eventually run into situations where two different goals are using an operator with the same name, and their apply rules will conflict. This is especially common with very generic steps – lots of goals may have an init operator. By testing the name of the goal in the apply rule, you avoid having apply rules from other goals firing in the wrong place.
In this example, two different goals have an operator named init, but these are not the same operator – they have different effects. Without the test for the name of the state in the apply rules, both states would get both effects, which is probably bad.
Here's an example in Michigan style (executable example here):
sp "my-goal1*propose*init (state <s> ^name my-goal1 -^initialized true) --> (<s> ^operator <o> +) (<o> ^name init) " sp "my-goal1*apply*init (state <s> ^name my-goal1 ^operator.name init) --> (<s> ^initialized true ^color blue) " sp "my-goal2*propose*init (state <s> ^name my-goal2 -^initialized true) --> (<s> ^operator <o> +) (<o> ^name init) " sp "my-goal2*apply*init (state <s> ^name my-goal2 ^operator.name init) --> (<s> ^initialized true ^color red) "
Here's the same example using NGS (executable example here):
sp "my-goal1*propose*init [NGS_match-active-goal my-goal1 <g> <s>] [NGS_is-not-tagged <g> initialized] --> [NGS_create-operator init <g> <o>] " sp "my-goal1*apply*init [NGS_match-active-goal my-goal1 <g> <s>] [NGS_match-operator init <o> <g> <gtags>] --> [NGS_tag <gtags> initialized true] [NGS_tag <gtags> color blue] " sp "my-goal2*propose*init [NGS_match-active-goal my-goal2 <g> <s>] [NGS_is-not-tagged <g> initialized] --> [NGS_create-operator init <g> <o>] " sp "my-goal2*apply*init [NGS_match-active-goal my-goal2 <g> <s>] [NGS_match-operator init <o> <g> <gtags>] --> [NGS_tag <gtags> initialized true] [NGS_tag <gtags> color red] "
Of course, there are other solutions; e.g., name these operators init-my-goal1 and init-my-goal2. This even has a (very slight) performance/memory advantage. But now goal information is encoded in the name of the operator, as opposed to directly using it. If the name of the goal changes, this could lead to a disconnect where the name of the operator doesn't make sense, etc. Another approach would be to add a goal-name augmentation (e.g., ^goal my-goal1) to the operators. In general, I think testing the name of the goal directly (either using a goal test or an operator augmentation) is preferable to directly encoding the goal name in the operator name, unless you have a special need.
Treat Operators and Goals like Function Calls
In Soar, all rules have access to all of working memory. This leads to code that treats all data as global (which is generally considered bad in most languages), with rules testing structures from all over memory. This makes it very difficult to know what the scope of an operator or goal is. This, in turn, makes the code hard to understand and maintain.
To improve this, I recommend explicitly linking an operator or goal to the data it will use and/or modify. For an operator, it might look like this (executable example here):
sp "my-goal*propose*add (state <s> ^name my-goal ^value1 <value1> ^value2 <value2>) --> (<s> ^operator <o> +) (<o> ^name add ^value1 <value1> ^value2 <value2>) " sp "my-goal*apply*add (state <s> ^name my-goal ^operator <o>) (<o> ^name add ^value1 <value1> ^value2 <value2>) --> (<s> ^result (+ <value1> <value2>)) "
In this case, all the data required by the operator is gathered in one place – the proposal. It's added to the operator, and thus the apply only need to look at the operator for the data it needs.
This can be taken a step further by specifying the result location in the operator as well. This is especially useful in floating operators (and subgoals) that may be used across many different goals. Here's an example demonstrating this (executable example here):
sp "my-goal*propose*add (state <s> ^name my-goal ^value1 <value1> ^value2 <value2>) --> (<s> ^operator <o> +) (<o> ^name add ^value1 <value1> ^value2 <value2> ^result-id <s> ^result-attr result) " sp "my-goal*apply*add (state <s> ^name my-goal ^operator <o>) (<o> ^name add ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) --> (<id> ^<attr> (+ <value1> <value2>)) "
The same general principles apply to goals and subgoals: pass them the data you expect them to use. For Michigan-style goals, I have a couple helper rules below. Essentially, you attach the arguments to the proposed operator just like we did above. When that triggers an operator no-change impasse, these rules will create an "args" structure in the subgoal and link/copy whatever is on the operator into that args structure:
##! # @brief # @type state-elaboration # # automatically copy any arguments from the superoperator to the local state sp {elaborate*state*operator-args-base (state <s> ^superstate.operator) --> (<s> ^args <args>) } ##! # @brief # @type state-elaboration # # automatically copy any arguments from the superoperator to the local state sp {elaborate*state*operator-args (state <s> ^superstate.operator.<att> <val> ^args <args>) --> (<args> ^<att> <val>) }
For example (executable example here):
sp "propose*my-goal (state <s> ^superstate nil -^result) --> (<s> ^operator <o> +) (<o> ^name my-goal ^value1 4 ^value2 5 ^result-id <s> ^result-attr result) " sp "my-goal*propose*add (state <s> ^name my-goal ^args <args>) (<args> ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) --> (<s> ^operator <o> +) (<o> ^name add ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) " sp "my-goal*apply*add (state <s> ^name my-goal ^operator <o>) (<o> ^name add ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) --> (<id> ^<attr> (+ <value1> <value2>)) "
Note in this example, the result-id and result-attr can be used to return a result to the my-goal superstate from the do-something-complex substate. It will not be obvious that the rule returning the result is modifying the superstate, since it won't directly test it. This is flexible, but may make the code harder to follow.
For NGS goals, you can do something similar, either using tags or adding WMEs directly to the goal structure. Here's an example using tags (executable example here):
sp "create*my-goal [NGS_match-goalpool <goals> <s>] -(<s> ^result) --> [NGS_create-goal <goals> my-goal <g> <gtags>] [NGS_tag <gtags> value1 4] [NGS_tag <gtags> value2 5] [NGS_tag <gtags> result-id <s>] [NGS_tag <gtags> result-attr result] " sp "my-goal*propose*add [NGS_match-active-goal my-goal <g> <s>] [NGS_is-tagged <g> value1 <value1>] [NGS_is-tagged <g> value2 <value2>] [NGS_is-tagged <g> result-id <id>] [NGS_is-tagged <g> result-attr <attr>] --> [NGS_create-operator add <g> <o>] (<s> ^operator <o> +) (<o> ^name add ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) " sp "my-goal*apply*add [NGS_match-active-goal my-goal <g> <s>] [NGS_match-operator add <o> <g>] (<o> ^value1 <value1> ^value2 <value2> ^result-id <id> ^result-attr <attr>) --> (<id> ^<attr> (+ <value1> <value2>)) "
Avoid Side Effects in Goals
This is another general rule of thumb; in practice, it may be much easier to follow it 90% of the time rather than 100% of the time. Ideally, a subgoal should only return results at the end of its processing. Goals that execute sequences of operators, many of which are returning results, are generally more difficult to follow and also more difficult to maintain – if the goal terminates partway through an expected sequence due to a returned result, is that a bug? It can be hard to tell.
For example, suppose you have a goal that is going to create a complex structure. The first operator creates the base structure, the second operator adds some stuff to it, the next operator adds more stuff to it, etc. One way to do this would be to directly construct the structure in the supergoal. Thus every operator is returning a result. Generally speaking, it would be better to construct the entire structure in the subgoal, and then return it to the supergoal once it is finished.
There are exceptions to this rule. Suppose that your goal can be interrupted such that the data structure you're working on could be destroyed (this is common in Michigan-style goals). Maybe it doesn't matter – you'll just make it again. But it could also be that it is either very expensive to create, or you expect to be interrupted constantly such that you'll never finish if you keep starting over, or perhaps the creation process has some side effects that you can't control (e.g., you need to retrieve the next available id from an external system and you don't want to skip any ids). In these cases, returning multiple results makes sense. Still, you might consider restructuring this into multiple goals, each of which does one part of the creation process (it's not clear if that's a good idea in many cases, but it should be considered).
There may also be situations where you can technically avoid side-effects, but it's a lot of work and/or inefficient. For example, suppose you have a goal that is going to modify several parts of a data structure in a supergoal. You may have separate operators for each part of the structure that is going to be changed, and thus it would be very easy to have each operator fire in sequence to make the modifications – i.e., a result is returned at each step of the process. To avoid this, you could copy the entire data structure in the subgoal, make the modifications, and replace the superstate data structure with the modified one. Indeed, functional programming languages (e.g., clojure) encourage this approach. But if the data structure is large, making a copy could be inefficient. Also, in NGS, other goals may link to parts of a shared data structure, making it difficult or impossible to replace. And there may be parts of the data structure that cannot just be copied (e.g., if it contains LTIs) – any changes to these substructures would be results.
In summary, I think it's a good idea to try to avoid side effects where practical, as it makes the code easier to understand. But I wouldn't get hung up on if its going to be a big hassle – after all, if you have to write complicated code to achieve this, then it at least partially defeats the purpose of making it easy to understand.
Soar "Tricks"
The stuff described here is nothing special to experienced Soar programmers, but may be helpful to new programmers.
When testing for greater/less than, test for the negation instead
Suppose you want to create a flag that indicates whether an input value is above 5. You might do something simple like this:
sp "elaborate*value-above-5 (state <s> ^io.input-link.value > 5) --> (<s> ^value-above-5 true) "
The potential issue here that every time the value changes, the rule will retract. It may immediately re-fire (i.e., if the new value is still above 5), but the value-above-5 wme will go away and immediately come back. This is called blinking. Whether this is a problem depends on your application, but generally speaking, we want to avoid blinking because most of the time it doesn't make conceptual sense, and it can be a performance issue. In the worst case, it can actually prevent your agent from working correctly.
To understand the conceptual issue, consider a value of 6. The rule will fire, and the flag will be created. Now the value changes to 7. The rule will retract, the flag will go away, then the rule will re-fire and the flag will be re-created. But the value was always above 5, so why did the flag ever go away?
Now consider if the value is changing frequently, even every cycle. This rule will be constantly retracting and re-firing, as will any rules that depend on the flag. This can result in a performance issue. Furthermore, because rules that depend on the flag will also retract, this could actually prevent the agent from making progress – for example, if a goal keeps getting retracted before the agent can make any progress, only to be immediately re-created.
To solve the issue, simply negate the test, like this:
sp "elaborate*value-above-5 (state <s> ^io.input-link <il>) -(<il> ^value <= 5) --> (<s> ^value-above-5 true) "
The way Soar's rete handles negations means that the flag will not blink in this case, so long as the value remains above 5.
As a side note, blinking is also an important issue in input-link design. For all the same reasons above, whether and when an input WME should blink needs to be carefully considered.
Avoid lots of redundant rule firings by mapping many objects into one object
Sometimes you want to do some action if there are any objects in a set. You could do this:
sp "propose*my-action (state <s> ^objects.object -^my-action-complete true) --> (<s> ^operator <o> + =) (<o> ^name my-action) "
This will propose my-action for every object in the set. One of them will be picked, and presumably the others will go away as a result of whatever the action is. This works, but is inefficient. If there are 100 objects in the set, 100 operators will be proposed, and there will be a noticeable slowdown in the decision procedure. This is even worse if you are testing multiple sets, as you will then get the combinatorial explosion of the two sets.
Instead, simply create a flag to indicate that there are objects in the set, and then have the operator proposal test that flag. The flag will only be created once since working memory is a set (it will simply be supported by multiple rule instantiations):
sp "elaborate*objects-exist (state <s> ^objects.object) --> (<s> ^objects-exist true) " sp "propose*my-action (state <s> ^objects-exist true -^my-action-complete true) --> (<s> ^operator <o> + =) (<o> ^name my-action) "
You might think this is no more efficient – after all, the elaboration rule is going to fire for every object in the set. But this is far more efficient than firing a rule that proposes an operator for every object and then making the decision procedure process all of the proposed operators. The decision procedure is not very sophisticated: it essentially cycles through the list of proposed operators repeatedly, throwing things out due to preferences when possible, and ultimately impassing or making a random decision. If the list of proposed operators is long, it will be slow to cycle through that long list multiple times.
This trick can also be useful for reducing the partial match cost of rules: if two sets are being crossed in a way that generates lots of partial matches (which is slow and takes lots of memory), it may be possible to map one of those sets into a flag and test that instead.
Common utility code
There is certain code that is very common across many agents. Many of the examples in this guide assume it is already loaded.
Attachments:
test-goal-name-in-apply-rules-ngs.soar (application/octet-stream)
treat-operator-like-function-call1.soar (application/octet-stream)
treat-operator-like-function-call2.soar (application/octet-stream)
treat-goal-like-function-call-michigan-style.soar (application/octet-stream)
treat-goal-like-function-call-ngs.soar (application/octet-stream)