In a networked environment, bandwidth and latency are typically the limiting factors for system performance. In order to improve efficiency in a networked environment, GFP defines an FRS and implementation language independent procedure language. The procedure language allows an application writer to combine several GFP operations into a single call. If that call requires transmission over a network, this results in a substantial performance boost.
For example, computing the information necessary to display a complete class graph for a knowledge base would require calling at least two GFP operations for each class (one to get its subclasses, and one to get a printable representation of its name). This could result in many thousands of calls to GFP operations. Using the procedure language, only a single network call needs to be made; the remaining calls are executed on the GFP server and only the results are transmitted back over the network.
The procedure language supported by GFP is expressed in a simple Lisp-like syntax and provides a dynamic binding model for its variables. The procedure language supports all of the GFP operations, and the ability to create and register new procedures, and a small number of programming constructs (such as conditionals, iteration, and recursion). No operations are supported within the procedure language that might compromise the security of a GFP server machine.
The basic object in the procedure language is the procedure. Procedures are created with create-procedure, which allows the parameter list, body, and an environment to be specified. Once created, a procedure is registered under a name using register-procedure. It is invoked using the call-procedure operation.
The following grammar specifies the syntax for the parameter list and
the body of a procedure. Literals in the grammar are enclosed in
string quotes, and token names are in upper case.
parameter-list ::= "(" parameter* ")"
parameter ::= SYMBOL
body ::= body-form*
body-form ::= procedure-call | simple | binding-form | loop-form
procedure-call ::= "(" SYMBOL body-form* ")"
binding-form ::= "(" ["LET"|"LET*"] "(" binding* ")" body ")"
binding ::= "(" SYMBOL body-form ")"
loop-form ::= "(" "DO-LIST" binding body ")"
simple ::= atom | quotation
quotation ::= "(" "QUOTE" form ")" | "'" form
form ::= atom | list
list ::= "(" form* ")"
atom ::= INTEGER | FLOAT | STRING | SYMBOL | true | false
true ::= "T" | "TRUE"
false ::= "NIL" | "FALSE" | "()"
In conditional expressions (e.g., if, while), any
non-false value is considered true just as any non-zero
value is considered true in the C language.
A string literal is enclosed in double quotes. All characters are
permitted within strings, including newlines and nulls; the double quote
character and the backslash character must be escaped with a backslash
character. The arguments to
create-procedure may be embedded within strings. This means
that the level of escaping required by the host
language will be necessary. For example, in Java, a
procedure body for the expression (f1 x "hello") is specified by
the string "(f1 x \"hello\")".
The procedure language is whitespace-insensitive, but some whitespace is necessary to delimit tokens other than parentheses and quotes. The INTEGER and FLOAT data types have their normal textual representation, with exponential notation (as found in Java). Semicolons introduce end-of-line comments. All characters following a semicolon are ignored until the next newline. Hash (#) characters are not permitted.
The SYMBOL data type has its origins in Lisp, but is supported by all GFP servers. Symbols are used as identifiers for variables and procedures, but they are also permitted as literals. Any non-control character can be part of the name of a symbol, with the exception of colons, semicolons, hashes, parentheses, quotes, or whitespace. A symbol may not start with a digit.
The namespace of symbols is partitioned into regions called packages. The procedure language is case and package insensitive except for symbol literals. It is an error for a portable program to assume the existence of any package other than the GFP or keyword packages in a procedure. A KB or FRS may define packages other than the keyword and GFP packages. There is no need to be aware of the packages defined for the KB unless the application specifically needs to test for object identity with symbol literals defined in the KB. If this is the case, it is the responsibility of the client program to create any necessary packages on the client side.
In order to refer to a symbol X in a package FOO, we write a double colon between the package name and the symbol: FOO::X. The keyword package holds many symbols that are used as literals. Keywords are used so frequently that they have a special syntax. The symbol X in the keyword package is spelled :X. In addition to their special syntax, every keyword is also a global constant whose value is itself. Thus, the value of the keyword :X is always the symbol :X.
Strings, numeric literals, and keywords do not need to be quoted, they denote themselves. A quoted literal xxx can be expressed either as (QUOTE xxx) or as 'xxx. For example, the symbol FRED would be expressed as 'FRED. A list containing the elements A, 42, and "Hello" would be expressed as '(A 42 "Hello"). All non-quoted symbols within a procedure body are either variable references or the names of operators.
Each body-form returns a value, although that value may be ignored. The value returned by a procedure is the value returned by its last body form. The first element in a procedure-call is the name of an operator. That operator is applied to the other body-forms in the procedure-call - its arguments. The arguments themselves may be other procedure-calls. For example, (F1 1 2) means ``apply the F1 operator to the numbers 1 and 2.'' In the C language, one would express this as F1(1, 2). Unlike C or Java, there are no infix operators in the procedure language. The equivalent of 1 + 2 in C is (+ 1 2). Most characters are permitted in symbols, so whitespace must be used to delimit them. For example, (+1 2), means ``apply the operator +1 to the number 2''.
Suppose that F1 is the name of a procedure whose parameter list is (x y) and whose body is (* x (+ y 2)). I.e., F1 returns the result of multiplying x by the sum of y and 2.
Variable names (symbols, such as x, above) denote values. Attempting to retrieve the value of a variable with no assigned value signals an error. Variables acquire values either through being set, or through being bound. Constructs in the language establish bindings (e.g., LET, DO-LIST). The simplest means of establishing a binding, however, is through being a parameter to a procedure. In the above example, the procedure F1 has x and y as its parameters. Inside this procedure, we say that x and y are bound to the values passed in to the procedure when it was called. If we made a call to F1 such as (F1 3 4), x would be bound to 3, and y would be bound to 4 during the execution of F1. On exiting F1, x and y are restored to the values they had before, if any. During the execution of F1, we may set the value of either x or y to any other value, but the original value will still be restored upon exit from F1. GFP operations are supported within procedures using the standard Lisp keyword argument notation. Thus, if we have a frame that is the value of the variable F, and an own slot that is the value of the variable S, we could get the value of that slot in F with the call
(get-slot-value F S :kb kb :slot-type :own)where kb is a variable denoting the KB in which F resides. Note that the procedure language syntax may differ from the GFP syntax in the implementation language's binding. For example, in Java code we would write the above call to get-slot-value as
kb.get_slot_value(F, S, _own);
We are now in a position to understand a somewhat more complicated procedure. The following Java code fragment creates, registers, and calls a procedure that will return a nested list structure representing the taxonomic structure below a given class down to a given maxdepth.
1 mykb.register_procedure(
2 mykb.intern("GET-TAXONOMY"),
3 mykb.create_procedure(
4 "(class depth maxdepth)",
5 "(let ((pretty-name (get-frame-pretty-name class :kb kb)))" +
6 " (if (>= depth maxdepth)" " +
7 " (list (list class pretty-name) :maxdepth) " +
8 " (list (list class pretty-name) " +
9 " (do-list (sub (get-class-subclasses " +
10 " class :kb kb " +
11 " :inference-level :direct)) " +
12 " (get-taxonomy sub (+ depth 1) maxdepth)))))" +
13 "))";
14 Cons classes = mykb.call_procedure(p, Cons.list(mykb._thing, 0, 4));
Line 4 specifies the parameter list. The procedure takes three
arguments: a class that is the root of the taxonomy, the current
depth, and the maxdepth. Lines 5-13 specify the
procedure body as a multi-line string. First a binding is introduced
for the variable pretty-name using the let operator. The
pretty-name is bound to the result of calling
get-frame-pretty-name on class, which was one of the
arguments to the procedure, and the variable kb. The kb
variable will be bound by the invocation of call-procedure on
line 14. In line 6, we check the depth. If it is greater than
or equal to the maxdepth argument, then we return a list whose
first element is itself a list of the class and its pretty name, and
whose second element is the keyword :maxdepth. This keyword is
being used as a flag to the caller that this class may have
subclasses, but that they were not explored due to the depth limit.
If the depth has not exceeded the limit, then we construct a return
value on line 8. It is also a list whose first element is also a pair
consisting of the class frame and its pretty name. The second
element, however, is a list containing one sublist for each subclass
of class. We collect the subclasses in line 9 by calling
get-class-subclasses using :inference-level :direct.
The do-list operator iterates over the list of subclasses,
binding sub to each one in turn, and executing
get-taxonomy recursively on each subclass in line 12. The
do-list returns a list with one value for each iteration. Line
14 actually invokes the get-taxonomy function on mykb
starting from
:thing, at depth 0, up to maxdepth 4. The result
will be a list such as
((fr0001 "thing")
((fr0002 "animal")
((fr0003 "mammal")
((fr0004 "feline")
((fr0005 "cat") :maxdepth))
((fr0006 "canine") ...))))
The class element of each pair will be returned as a
frame-handle, whose appearance will differ across implementations.
In addition to the GFP operations defined in Section 3.7, the procedure language supports the following forms.
*
Diadic multiplication of numbers.
(* 42 2.5) = 105.0
&& operator in C or Java. A
conjunct is true if it is not NIL-valued. The whole
AND expression returns NIL immediately after
finding the first NIL conjunct.
(do-list (var <<list expression>>)
<<body form 1>>
<<body form 2>>
...
<<body form n>>)
For example,
(do-list (x '(1 2 3 4 5))
(+ x 100))
will return (101 102 103 104 105).
(let ((<<var1>> <<val1>>)
(<<var2>> <<val2>>)
....
(<<varn>> <<valn>>))
<<body expression-1>>
<<body expression-2>>
....
<<body expression-n>>)
All the <<vali>> expressions are evaluated before
the bindings for the variables are established. I.e., it is as if
the <<vali>> are evaluated in parallel. The value
returned by the LET expression is the value of the
last body expression. For example,
(let ((x '(a b c d))
(y 2001))
(push 100 x)
(push y x)
x)
will return (2001 100 A B C D).
(let* ((<<var1>> <<val1>>)
(<<var2>> <<val2>>)
....
(<<varn>> <<valn>>))
<<body expression-1>>
<<body expression-2>>
....
<<body expression-n>>)
Each <<valN>> expression is evaluated and a binding
is established for <<varN>> before the system
proceeds to the next binding. The value returned by the
LET* expression is the value of the last body
expression. For example,
(let* ((x '(a b c d))
(y (list* 2001 x)))
(push 100 x)
(list x y))
will return ((100 A B C D) (2001 A B C D)).
LET* is
equivalent to a series of nested LET expressions, so
(let* ((x 42)
(y (list x 200)))
y)
is equivalent to
(let ((x 42))
(let ((y (list x 200)))
y))
|| operator in C or Java. A disjunct is true if it is
not NIL. The whole OR expression returns the
value of the first non-NIL disjunct.
(while <<condition expression>>
<<body form 1>>
<<body form 2>>
...
<<body form n>>)
For example,
(while (has-more enumerator) (push (next enumerator) result))will collect all the values in the enumerator by pushing them onto the list called result. Note that this will build a list in the reverse order of the list built in the example for while-collect.
(while-collect <<condition expression>> <<body form 1>> <<body form 2>> ... <<result body form>>)For example,
(while-collect (has-more enumerator) (next enumerator))will collect all the values in the enumerator.