SPARK in a Nutshell
A Guide for Writing SPARK Packages in SPARK-L
by Alyssa Glass
v0.2 / 19 September 2005 / compatible with SPARK v0.8
v0.1 / 25 August 2004 for SPARK v0.4
0. Contents
1. Introduction
2. Installation and Set-Up
3. SPARK Packages
4. Declarations
5. Symbols and the Belief Knowledge Base
6. Writing Basic Procedures
7. SPARK Utility Packages
8. Closures
9. Advanced Procedure Definitions
10. The SPARK Meta-Level
11. Providing Python Implementations
12. SPARK Scripting
A. Index
1. Introduction
This document is for beginning SPARK-L writers, to assist with the creation of new packages. When first getting started writing SPARK packages, many new users find that the complete technical specification is too detailed in the language and does not give enough of a high-level view of how to approach SPARK packages. That is where this guide becomes helpful. It is recommended that new SPARK package writers start with this guide.
Note, however, that this guide is not meant to be exhaustive of the SPARK-L language. For a complete technical specification of the SPARK-L language, please see The SPARK Reference Manual, which is included in the /doc directory of the SPARK release. We recommend that once you are comfortable with the language as described in this guide, you then turn to The SPARK Reference Manual as a full language reference.
If you just want to be able to run existing packages, please see the install guide and the interpreter tutorial, both of which are included in the /doc directory of the SPARK release.
2. Installation and Set-Up
For help with installing SPARK on your machine, please see the install guide included with the release documentation. That document will guide you through a full installation of SPARK.
When you have finished installing SPARK, we also recommend that you work through the interpreter tutorial, which will walk you through using the SPARK interpreter, and will help you to become comfortable with loading pre-existing packages and interacting with a SPARK agent in the interpreter.
For the rest of this guide, we will assume that you have SPARK correctly installed with a working SPARK interpreter, and that you know how to run the interpreter to test SPARK packages. We also assume that you have an editing environment with which you are comfortable. For editing SPARK-L files, we recommend using the integrated SPARK Eclipse environment; see the SPARK Plugin for Eclipse documentation for help with installing and using SPARK Eclipse. Your SPARK release also contains an Emacs SPARK mode; see the spark-mode in Emacs section of the Developers Resources Guide for advice on editing SPARK files in Emacs.
3. SPARK Packages
Packages are the organizing structure of all SPARK files. A single package can be made up of one or more SPARK files. All of the files that make up a package share a common namespace, as described below. The set of files that make up a package is often declared when the package is loaded, but it does not have to be; packages can be expanded at runtime as needed.
SPARK files are written in the SPARK language, SPARK-L, and end in a ".spark" extension. These .spark files may contain any or all of the following constructs:
a. package statement
b. declarations
c. definitions
d. additional facts/beliefs for the knowledge base
e. import statements
f. export statements
g. requires statements
h. programmer comments
We will go through each of these constructs in turn.
a. package statement
The first line of most .spark files is a package statement, which indicates which package this file is to be a part of. To make this file part of the sri.foo package:
package: sri.foo
If no package statement is included in the file, the package is assumed to be the same as the filename. Thus, if the file mySparklCode.spark does not contain a package statement, it is automatically in the mySparklCode package. It is considered good practice to always place a package statement at the top of SPARK-L files.
One note about organizing multi-file packages. It is good practice in SPARK to place files in a common directory with other files in the same package. These packages are generally named by their path from a root path defined upon starting the interpreter. Thus, if C:/SPARKROOT/src/ is on the interpreter's path at startup, then the package "spark.lang.list" would often be made up of the files in the C:/SPARKROOT/src/spark/lang/list/ directory, and the associated files would each have a "package: spark.lang.list" statement.
One additional note, concerning backwards compatibility. In previous versions of SPARK, files were organized into modules. As of release 0.8 of SPARK, module statements ("module: sri.foo") are considered equivalent to package statements; however, this use is deprecated.
b. declarations
SPARK deals with objects such as predicates, actions, functions, and constants. In SPARK-L these objects are named by identifiers, such as foo. To be able to reference an object using an identifier, there must be a declaration of the object. The declaration states that there exists a named object and states how that object is used (for instance, foo is an action, it takes three arguments, etc.). The identifier is a unique name within a package, but different packages may use the same identifier to refer to different objects. To distinuish between the object with identifier foo declared in package a.b from that declared in package c.d, each named object has a fully qualified name that is a distinct symbol, in this case a.b.foo and c.d.foo. These declarations are immutable; they cannot be changed once execution has begun. Details on the syntax of the various types of declarations are in a later section.
c. definitions
Once a predicate, action, or function has been declared, it will also need to be defined. While the declaration provides basic information about its use, the definition provides the actual implementation. For example, for the function "+" (addition), the declaration might establish the identifier "+" as the name of the function, and indicate that the function takes 2 parameters and returns a value. The definition would actually define what it means to add two numbers. Details on the syntax of definitions are in a later section.
d. additional facts/beliefs for the knowledge base
In addition to declarations and definitions, you may want to initialize your SPARK agent with initial beliefs about the world. A package can thus contain statements of fact that will be inserted into the agent's knowledge base when that package is loaded. Additional information about SPARK's knowledge base of agent beliefs can be found in a later section.
e. import statements
To use SPARK constructs defined in a package other than the one you are currently working in, you need to import those constructs into the current package. The best and cleanest way is to import just the symbols that you will need to use. For instance, the spark.lang.list package, which provides many list manipulation functions, includes a predicate for checking whether an object is an element of a list. To import just the "Member" symbol from spark.lang.list:
importfrom: spark.lang.list Member
This makes any occurrence of the identifier Member in this file (or in any other file in the same package) refer to the declaration from package spark.lang.list
Alternatively, if you would like access to all symbols which have been exported from a given package, you can use the "importall:" command, without specifying any symbols:
importall: spark.lang.list
The order of import statements in a .spark file does not have any impact on the behavior of the package.
Since all files in a package share a common namespace, once this symbol has been imported by a single file, it will be visible to all files that make up that package. More information on the link between identifiers and the symbols they represent can be found in a later section.
f. export statements
By default, any symbols declared or defined in a .spark file are only known within that file's package. If you would like to be able to use a symbol in a different package, through an import statement in that package, you need to export the symbol. For example, if you are creating package foo.bar, and you want to export the symbol "my_sym" you would type:
export: my_sym
The symbol my_sym would then be accessible in another package which includes either ONE of these commands:
importfrom: foo.bar my_sym
importall: foo.bar
[FOR ADVANCED USERS: There is also a command exportall: which can be used to export all symbols declared in a given file. A warning: exportall can be tricky to use, because you often end up exporting more symbols than you intend, and not exporting functionality which you intended to export, which can cause strange behavior when you then use importall to import everything from the package. Use with caution. In general, exportall is NOT recommended in most situations.]
g. requires statements
Anything declared in or imported into a file F in package P is potentially visible to to any other file in package P. However, SPARK cannot let you use an identifier unless it knows where to find the declaration. The files containing the declarations you need could be located anywhere in the file system, and SPARK needs to be told where to find them. The requires: statement tells SPARK to load a file that contains declarations that you need.
Usually, for each package P there is a set of files containing core declarations. (Other files may add extra identifier declarations to the package, but the files containing the core declarations are always required.) For each package P there should be a file P.spark that either contains the core declarations itself, or includes requires: statements listing the files that contain the core declarations. Importing declarations from a package P automatically "requires" the P.spark file (as does a package: P statement). Thus, the main use for the requires: statement is in the P.spark file to state the core files of a package. (The requires: statement can also be used to tell SPARK where to find additional declarations that are not core declarations for a package.)
To access the facts (including definitions) contained in a given file, that file must be required by one of the packages loaded into the agent. To load the facts in the file foo.bar.spark:
requires: foo.bar
The distinction between "imports" and "requires" can be confusing to new SPARK users. A few points to help understand this distinction:
- Declarations of identifiers are imported from packages into packages. Files require other files.
- When declarations are imported from a package P, the file P.spark is automatically required (along with any files P.spark requires, and so on). The reverse is not true; requiring a file does not cause anything to be imported.
- When a package is imported from, the relevant declarations in that package are added into the importing package. When a file is required, it does not directly affect the file's package - the declarations in the file are only visible if the file is in the same package or the declarations are imported separately.
- When it comes time to load the definitions and other facts from a file into the agent's knowledge base, SPARK ensures that any required file is also loaded.
When deciding whether to use import or requires, the important consideration is whether you need access to new symbols, or just new facts about existing symbols already in your namespace. In most cases other than a file establishing core package files, you will want to use import. If you need to add new symbols to your package's namespace (for example, if you want to write a new procedure definition for an action declared in a different package, or if you want to assert a fact using a predicate declared in a different package) then you will need to use import to bring those symbols into your namespace.
h. programmer comments
Finally, you may add comments to .spark files to aid in readability and to ease maintaining your SPARK packages. In SPARK-L, programmer comments are indicated with a "#" and continue to the end of the line:
# everything on this line is a comment.
4. Declarations
Four types of identifiers must be declared in SPARK: actions, predicates, functions and constants. Failure to provide declarations will result in a warning when you try to load the package. In their most basic form, all four types of declarations have the same format.
Before we go into the syntax of declarations, a few overall syntax notes:
- variables in SPARK are indicated using dollar signs ($). For example, $foo is a variable.
- strings are indicated with double quotes: "this is a string"
- lists are indicated by square brackets [] with items separated by spaces. For example:
[item1 item2 item3]
a. actions
Use actions when you want your agent to "do" something. As their name suggests, actions are objects representing tasks that an agent can do. Later on, when we look at definitions, we will discuss how to write procedures which implement these actions. For now, you can think of action declarations as a catalogue of the actions that a SPARK agent knows how to do.
The general form of an action declaration:
{defaction (myAction $var1 $var2)
doc: "myAction takes two variables and performs some sort of action"}
Action declarations are contained in curly braces {}. They always start with the keyword defaction, followed by a parenthesized indication of the action's identifier and its arity. In the above example, the declared action is indicated by the identifier myAction and takes two variables.
To indicate that variables passed into an action must be evaluable (that is, must be passed in with a bound input value), place a
"+" sign in front of the variable. For example:
{defaction (mySecondAction +$var1 +$var2 $var3)
doc: "mySecondAction takes three variables; the first two are inputs"}
When mySecondAction is called, the first two inputs must be evaluable. Note that variables without a + sign can still be evaluable, even though they are not required to be. Marking input values with a +, however, provides for more readable code and better automatic error checking, and is encouraged for all action declarations with parameters that are known to be input values.
By default, all declarations that appear in a file are automatically shared (added to the namespace) of the entire package. To create a declaration that is only visible in the current file, use a "_" to start its identifier. For example:
{defaction (_myPrivateAction +$var1 $var2)
doc: "_myPrivateAction is local to this file"}
Action declarations also take a variety of optional keywords. Advanced users can look in the more detailed The SPARK Reference Manual to see some of these keywords. For now, we will just mention the doc keyword, which allows you to provide documentation for the use of this action. The automated SPARKDOC utility included with the release can be used to take the string following the doc keyword and create automated documentation web pages for your packages.
b. predicates
Predicates are used to test a SPARK agent's beliefs. They can either be defined programmatically or can be implicitly defined through the assertion of beliefs into the agent's knowledge base. Predicate declarations are completely analagous to action declarations. For example:
{defpredicate (MyPred $var1 $var2)
doc: "tests whether $var1 and $var2 are related with the MyPred relation"}
Like action declarations, predicate declarations are contained in curly braces {}. They begin with the keyword defpredicate, followed by a parenthesized indication of the predicate's symbol and its arity. Parameters in predicate declarations should be annotated with the + input symbol when possible, as with action declarations. A predicate can also be made local to the file by prefacing the predicate identifier with a _.
It is customary practice in SPARK-L to always use capital letters to begin the name of a predicate, to help to visually distinguish them from actions and functions when reading a package. They also have optional keywords which may be used to expand the predicate's signature. As with actions, we recommend always including the doc keyword to ease the creation of automated documentation.
The above declaration will allow statements with the predicate MyPred to be inserted into an agent's knowledge base, thereby implicitly defining it. If you would prefer to define it programmatically, you can use the "imp" keyword, described later in this guide.
c. functions
Functions in SPARK are declared using the deffunction keyword, and are otherwise analagous to the above examples:
{deffunction (myFun $var1 $var2)
doc: "applies myFun to $var1 and $var2 and returns the result"}
As above with predicates and actions, function declarations have additional optional keywords which may be used to expand a function's signature. In particular, the "imp" keyword is generally used to define functions within a .spark file. The use of the "imp" keyword is described later in this guide.
d. constants
Declarations for constants are more simple than the declarations above. Constants are declared with the defconstant keyword, and simply take the name of the identifier as an argument:
{defconstant person23}
5. Symbols and the Belief Knowledge Base
Every SPARK agent maintains a single knowledge base containing its beliefs about the world. For now, this knowledge base only contains current beliefs; it does not maintain a history of past beliefs.
An agent's knowledge base of beliefs includes many kinds of facts. Some beliefs are loaded automatically by the SPARK engine for every agent, such as beliefs about built-in procedures, predicates and functions. Some are added (or deleted) as the agent performs procedures. Others are directly added by loading new packages. If we load a package with the three declaration examples from above, we will be adding beliefs about myAction, MyPred, and myFun to the agent's knowledge base.
We can also add specific predicate beliefs to initialize an agent upon start-up. For instance, the above declaration for MyPred does not include an implementation for how to test MyPred. If we want to define MyPred implicitly by inserting facts, we can include these initial facts directly into our .spark file. For example, the following means that MyPred is true if its two parameters are (1 and 2), (12 and 7), or (16 and 7):
(MyPred 1 2)
(MyPred 12 7)
(MyPred 16 7)
For any predicate which we have either declared or imported from another package, we can declare initial beliefs in this way.
To record facts in the knowledge base that link to a single symbol, create the symbol using the defconstant declaration above. To create symbols on the fly, we can backquote an identifier:
`myID
Note that backquoting does not create the same symbol as defconstant, even when given the same identifier as input. For example, consider these two lines of SPARK-L code:
{defconstant: person14}
`person14
The first of these lines declares the constant person14 in the package in which the declaration appears and the identifier person14 becomes part of the current package's namespace. This constant evaluates to the symbol [package].person14. The second line creates a symbol on the fly, outside of any package. Its full name is simply person14 and its identifier is not part of any package's namespace.
If a constant already exists in your current namespace, either through a declaration in your current package or through an import, you can refer to that constant directly. For example, suppose we want to store several facts about specific people. We create a constant for each person, then link all of the facts about that person off of the symbol value of that constant:
{defconstant: person23}
(HasName person23 "Bill Jones")
(HasPhoneNumber person23 "650-555-1234")
(HasEmailAddress person23 "bill@starbob.com")
Backquoting creates symbols on the fly by not evaluating the name that follows it. In this way, we can use backquote to prevent the evaluation of functions or predicates as well. For example:
`(store 1 2 3)
evaluates simply to (store 1 2 3) regardless of whether the identifier store is in the current namespace. To force the evaluation of individual terms within a backquote, use a , (comma). For example:
`(store 1 ,(+ 7 8) 3)
evaluates to (store 1 15 3).
To create a symbol from a string representation, you can use the @ functor:
(@ "person14")
Note that the symbols created by (@ "person14") and the simpler `person14 are equivalent. The former method is included for instances where symbols need to be created on the fly from unknown string input. Similarly, the functor @@ can create functions on the fly:
(@@ "store" 1 2 3)
The above evaluation is equivalent to `(store 1 2 3) described above, and is used analagously to @ for creating on-the-fly functors from strings.
After the initialization of a SPARK agent, the knowledge base can be updated in two ways. One way is to add or remove beliefs directly, as through the SPARK interpreter. The other way is through the effects of procedures, described below.
6. Writing Basic Procedures
At this point, the main outline of your package is in place: you have organized your work into logical .spark files; you have declared any actions, predicates, and functions that you want to create; you have decided which of them you want to export to other packages; and you have initialized your agent with any initial beliefs that you want loaded at startup. In most cases, the majority of what you have declared are actions and implicitly-defined predicates. The main content of your package, then, still remains: writing definitions for your actions. This is done through the writing of procedure definitions, in which you use various preconditions and task expressions to provide mechanisms for performing your actions.
The basic parts of a procedure definition are:
a. name
b. cue
c. precondition
d. body
Here is an example of a simple procedure definition:
{defprocedure paintHouse_havePaint
cue: [do: (paintHouse $color)]
precondition: (HavePaint $color)
body: [seq: [do: (placeDropCloths)]
[do: (applyPaintCoat $color)]
[do: (cleanUp)]]
}
We will go through the basics of each of these parts in turn. But first, some notes on binding variables and finding solutions.
As mentioned above, variables in SPARK are indicated with a dollar sign ($) at the beginning of their names. These variables are not rebindable; once they have been bound to a value through their use in an action, predicate, or function, they may not be bound again (unless the task that bound the variable fails or the variable is local to an iteration).
As you write procedure definitions, you will need to pay close attention to which variables are bound, when they are bound, and what they are bound to. When you load a package into SPARK, the SPARK engine will warn you if you are using a variable as if it were bound even though it may not be.
In the interpreter tutorial, you learned that when you evaluate a logical expression, all free variables are bound to the first valid solution that the SPARK engine is able to find. Keep this in mind as you write procedure definitions: at each step of the procedure, free variables will be bound to the first valid solution found, and the variables will remain bound to those values for the remainder of the procedure. In the paintHouse_havePaint example above, the variable $color will be bound upon testing of the precondition, if it was not already bound when the procedure was called, and that value will be used during the execution of the tasks that follow it.
a. procedure name
Procedure definitions, like declarations, are enclosed in curly braces {} and begin with the keyword defprocedure. They are named with an identifier or string, which should be unique across the package.
b. cue
A procedure is relevant when its cue event occurs. There are 3 types of cue events in SPARK:
1. direct request for action (a do cue event)
The above paintHouse_havePaint example is an example of a procedure with a do cue event. Cues of this type always have the form [do: (action_identifier variable*)] as above. In most situations, these cues are the most common procedure cue events.
2. a new fact has become true (a newfact cue event)
Procedures can also be triggered when a particular fact is added to the agent's belief knowledge base. Cues of this type have the form [newfact: (predicate_identifier variable*)]. For example, if there is a procedure that should be performed every time the predicate (Happy $person) is added to the knowledge base, the cue would be:
[newfact: (Happy $person)]
3. a predicate is false and needs to be achieved (an achieve cue event)
The least common type of cue event is triggered when a procedure needs to be triggered in order to achieve a currently false predicate. Cues of this type have the form [achieve: (predicate_identifier variable*)] and are generally used for procedures for which (predicate_identifier variable*) is added to the knowledge base as a result of the actions in that procedure. For example, if there is a procedure that concludes with the fact (Happy $person) becoming true, it would have the cue:
[achieve: (Happy $person)]
Note that do cue events must refer to actions which have been declared or imported in the current package. Similarly, newfact and achieve cue events must refer to predicates which have been declared or imported in the current package.
All of the above cue types can have any number of parameters associated with them. As with declarations described above, the parameters to a procedure cue can be annotated to indicate the mode in which the procedure is expected to be called. For instance, the above paintHouse_havePaint procedure can be called with the parameter $color either bound or unbound. If we want to guarantee that the procedure is always called with $color already bound, we could mark it as an input parameter by annotating it with a + prefix:
[do: (paintHouse +$color)]
Input parameters are not restricted to being variables. In fact, any term expression can be supplied as an input parameter and it will be matched against the supplied input value. For instance, if we want our paintHouse procedure to only handle blue houses, we could change the cue to:
[do: (paintHouse +"blue")]
This procedure would then only be triggered if the action was called with the parameter "blue".
Suppose we have declared an action named processList with one parameter. If we want to write a special procedure definition for this action that only handles two-element lists, we could use the following cue:
[do: (processList +[$x $y])]
The procedure with the above cue would then only be triggered when called with two-element lists, and we would have immediate access to the two elements, which would bind $x and $y.
Older versions of SPARK did not have parameter annotations. Instead it was necessary to use the Ground predicate to test whether a parameter was bound, or to assert that a parameter was always an input value. This use of the Ground predicate is still allowed, but is not encouraged in most situations. Instead, using annotations to clearly mark parameters results in much more efficient execution and helps the reader understand the intention of the procedure.
A parameter can be marked as an output parameter by annotating it with a - prefix. This indicates that rather than the caller supplying a value that will be used within the procedure, the parameter value is to be constructed by the procedure and returned to the caller. As with input parameters, an output parameter is not restricted to just being a variable. Any expression that can be evaluated when the procedure ends can be used as an output parameter. For example, consider this cue:
[do: (paintHouse -"blue")]
A procedure with the above cue always be triggered by a paintHouse action, without regard to the actual parameter supplied by the caller. After successful execution of the body of the procedure, the parameter supplied by the caller will be matched to "blue". If it does not match, the procedure will fail.
c. precondition
A procedure is applicable when its precondition logical expression is true. Note that being relevant and being applicable are different concepts: a relevant (cue event has occured) procedure is only applicable if its precondition is also true.
Preconditions help to determine which procedure to use in a situation. For example, a single action declaration might have many different procedures that trigger off of it. Preconditions allow you to specify which procedure to use in different situations.
A precondition can be any logical expression. A logical expression could be any of the following:
- any previously declared predicate
- (True) or (False)
ground predicates that are always true/false
- logically connected logical expressions
SPARK-L supports and, or, and not as logical connectives. Examples:
(and (> 4 $x) (!= $x 2))
(or (AtHome $person) (AtWork $person) (AtSchool $person))
(not (= $x 5))
- existential quantifier
using the keyword exists and a list of variables. For example:
(exists [$person] (AtHome $person))
- applied predicate closures
closures will be discussed in a later section. For now, just be aware that applying a predicate closure to arguments will also create a logical expression.
Procedures that should be considered applicable in any relevant situation would have precondition: (True). For simplicity, these (True) preconditions may be optionally left out of the procedure definition.
In situations where there is more than one applicable procedure for a given cue, the SPARK meta-level determines which procedure to use. A discussion of the meta-level occurs in a later section of this guide.
Remember that each statement with free (non-local) variables in a procedure will bind those variables upon completion. Thus, for any applicable procedure that is chosen to be executed, the variables that are bound in the precondition will be used in the body as well.
d. body
The body of a procedure definition contains a task expression indicating what should be done to execute the procedure. We will list the most common ways to build up a task expression, with simple examples illustrating each one. In the following, term refers to a term expression, var refers to a variable, log referes to a logical expression, task refers to a task expression, and log-task refers to a logical expression followed by a task expression.
- [do: (action_identifier term*)]
-
perform the named task
Example:
[do: (applyPaint wall red)]
- [achieve: (predicate_identifier term*)]
-
attempt to make the given predicate true, if it isn't already
Example:
[achieve: (ColorOf wall red)]
- [succeed:] or []
-
do nothing:
[succeed:]
- [fail: term]
-
immediately fail, specifying some value as a reason
Example:
[fail: outOfPaintError]
- [context: log term*]
-
a context task expression does not perform a task like most task expressions, but rather binds variables in a logical expression with solutions to that expression. Recall that logical expressions are defined above in the discussion of procedure preconditions. If the logical expression has no solutions then the context task expression fails. If an optional format string and arguments are supplied, then a formatted message is printed out as well.
Example:
[context: (HasParent $x $y) "Person %s has no parent" $x]
The context expression is extremely useful when combining task expressions into complex task expressions, as explained below. context expressions allow you to retrieve facts from the knowledge base for use in complex task expressions.
- [set: var term]
-
like context expressions, a set expression does not perform a task, but rather binds a variable to a value specified in TERM.
Example:
[set: $x (+ 3 4)]
Note that the above expression is equivalent to:
[context: (= $x (+ 3 4))]
when $x is not previously bound. set expressions are therefore only included for convenience, to make it clear in your code when a variable is explicitly being set.
- [seq: task*]
-
perform the task expression(s) in sequence
Example:
[seq: [context: (and (MyFavoriteColor $color1) (YourFavoriteColor $color2))]
[do: (applyPaint wall $color1)]
[do: (applyPaint door $color2)]]
Note the use of context to bind $color1 and $color2 before executing the rest of the task expression.
- [parallel: task*]
-
perform the task expression(s) in parallel
Example:
[parallel: [do: (rubStomach Bob)]
[do: (patHead Bob)]]
- [select: log-task*]
-
given a series of pairs of logical expressions and task expressions, perform the first one whose logical expression has a solution. If none of them have a solution, then fail.
Example:
[select: (InCar Bob) [do: (drive Bob home)]
(True) [do: (talkOnCellPhone Bob)]]
- [wait: log-task*]
-
like select above, but if none of the logical expressions have a solution, wait until one does instead of failing.
Example:
[wait: (AtHome Bob) [do: (talkOnCellPhone Bob)]]
- [while: [variable*] log task]
-
repeatedly test a logical expression and execute a task until the logical expression no longer has a solution.
Example:
[while: [$x] (and (MyPet $x) (Hungry $x)) [do: (feed $x)]]
Note that all free variables in the logical expression and the task expression that are not in the local variable list must be bound before performing the task.
- [forall: [variable*] log task]
-
find all solutions to a given logical expression and then perform the given task for each solution.
Example:
[forall: [$person] (InCar $person) [do: (drive $person home)]]
Note that, like with while expressions above, all free variables in the logical expression and the task expression that are not in the local variable list must be bound before performing the task.
- [forallp: [variable*] log task]
-
find all solutions to a given logical expression and then perform the given task for each solution in parallel.
Example:
[forallp: [$person] (InCar $person) [do: (drive $person home)]]
- [forin: variable list task]
-
for each element in the given list, perform the given task, with the variable bound to that element.
Example:
[forin: $x [A B C] [do: (print "Got %s!" [$x])]]
The above will result in the printing of "Got A!" "Got B!" and "Got C!" in sequence.
Special effects
Special effects task expressions have a direct effect on the belief knowledge base, directly adding or removing statements of belief. There are three effects task expressions, and when combined with other task expressions (for instance, in a seq expression) they generally come last.
Note that for all of these effects task expressions, all of the variables in the predicates that are not in the local variable list must be bound before executing the task expression.
- [conclude: (predicate_identifier term*)]
-
add a fact to the knowledge base
Example:
[conclude: (Happy Bill)]
- [retract: (predicate_identifier term*)]
-
remove a fact from the knowledge base
Example:
[retract: (Scared Bill)]
- [retractall: [variable*] (predicate_identifier term*)]
-
remove all facts from the knowledge base that match a given predicate pattern
Example:
[retractall: [$person] (Scared $person)]
SPARK-L also contains special functions that provide conditionality for use in task expressions. While some conditionality can be achieved with the task expressions described above (for instance, with select or forall task expressions) these functions provide greater flexibility for testing conditions, looping over solutions, and building lists of results.
- (solutionspat [var*] log term)
-
return a list consisting of the evaluation of term for each var* that satisfies log. solutionspat is similar to solutions above, with the difference that you can specify an arbitrary term to be included in the list for each solution.
Examples:
(p 1 2)
(p 3 4)
(p 3 5)
[set: $sols1 (solutionspat [$x $y] (p $x $y) [$x $y])]
[set: $sols2 (solutionspat [$x $y] (p $x $y) $y)]
[set: $sols3 (solutionspat [$x $y] (p $x $y) "Found one!")]
After the evaulation of each of these task expressions, the variables $sols1, $sols2, and $sols3 would have the following bindings:
$sols1: [[1 2] [3 4] [3 5]]
$sols2: [2 4 5]
$sols3: ["Found one!" "Found one!" "Found one!"]
- (if log term term)
-
if log has a solution, returns the first term, else returns the second term.
For example, given the same 3 facts as above:
(p 1 2)
(p 3 4)
(p 3 5)
[set: $sols (if (p 1 2) "yes" "no")]
After the evaluation of this task expression, the variable $sols will be bound to "yes". While this same behavior can be achieved using the select task expression described above, having conditionality available as a function greatly increases the expressability and power of task and predicate closures, discussed below.
7. SPARK Utility Packages
To aid in creating packages, SPARK comes equipped with several packages containing utility predicates, actions, and functions that are broadly useful across many packages. Full SPARKDOC documentation for these packages can be found in your release in /doc/sparkdoc/. We mention several of the most generally useful ones here, but refer to the full documentation for additions.
First, a few generally useful built-in utilities that you will automatically have access to without needing any imports:
- basic mathematical functions: +, -, *, /, mod, and sort
- basic comparative predicates: =, !=, >, <, <=, and >=
- print -- formatted print task. Example:
[do: (print "My name is %s and my favorite color is %s." [$name $color])]
- genid -- task to generate a unique id
The following utilities are not automatically loaded. You will need to import these packages into your file when using them.
spark.lang.list
This package contains utilities for dealing with lists in SPARK. See SPARKDOC for usage. The most commonly used list utilities:
- list predicates: Empty, Member, Concat, Index, Length
- list function: mapCall (like mapcar in Lisp)
spark.util.util
[This spark.util.util package is still under construction. Please see SPARKDOC for details.]
With the above knowledge of SPARK, you are well on your way to writing your own packages. The rest of this guide contains more advanced topics. You are encouraged to stop here and get your "feet wet" with SPARK-L before continuing with the rest of this guide.
11. Providing Python Implementations
It is possible to declare SPARK predicates, functions, and actions
whose behavior is determined by Python or Java code. This is achieved
by specifying an imp: attribute on
a defpredicate, deffunction, or defaction
respectively. For example, to define > as a predicate
taking two parameters whose truth is determined by the Python
function __gt__ in the Python module operator you
would write:
{defpredicate (> $x $y) imp: (pyPredicate "++" (pyMod "operator" "__gt__"))}
More detains can be found in Section 7 of
The SPARK Reference Manual
A. Index
last edited 15 September 2005.
Copyright (c) 2004, SRI International. All rights reserved.