In section 4.4.4 we will present an implementation of the query interpreter as a collection of procedures. functions. In this section we give an overview that explains the general structure of the system independent of low-level implementation details. After describing the implementation of the interpreter, we will be in a position to understand some of its limitations and some of the subtle ways in which the query language's logical operations differ from the operations of mathematical logic.
It should be apparent that the query evaluator must perform some kind of search in order to match queries against facts and rules in the data base. One way to do this would be to implement the query system as a nondeterministic program, using the amb evaluator of section 4.3 (see exercise 4.89). Another possibility is to manage the search with the aid of streams. Our implementation follows this second approach.
The query system is organized around two central operations, called pattern matching and unification. We first describe pattern matching and explain how this operation, together with the organization of information in terms of streams of frames, enables us to implement both simple and compound queries. We next discuss unification, a generalization of pattern matching needed to implement rules. Finally, we show how the entire query interpreter fits together through a procedure function that classifies expressions queries in a manner analogous to the way eval evaluate classifies expressions for the interpreter described in section 4.1.
A pattern matcher is a program that tests whether some datum fits a specified pattern. For example, the data list ((a b) c (a b)) datum list(list("a", "b"), "c", list("a", "b")) matches the pattern (?x c ?x) list($x, "c", $x) with the pattern variable ?x $x bound to (a b). list("a", "b"). The same data list data list matches the pattern (?x ?y ?z) list($x, $y, $z) with ?x $x and ?z $z both bound to (a b) list("a", "b") and ?y $y bound to c. "c". It also matches the pattern ((?x ?y) c (?x ?y)) list(list($x, $y), "c", list($x, $y)) with ?x $x bound to a "a" and ?y $y bound to b. "b". However, it does not match the pattern (?x a ?y), list($x, "a", $y), since that pattern specifies a list whose second element is the symbol a. string "a".
The pattern matcher used by the query system takes as inputs a pattern, a datum, and a frame that specifies bindings for various pattern variables. It checks whether the datum matches the pattern in a way that is consistent with the bindings already in the frame. If so, it returns the given frame augmented by any bindings that may have been determined by the match. Otherwise, it indicates that the match has failed.
For example, using the pattern Using the pattern (?x ?y ?x) list($x, $y, $x) to match (a b a) list("a", "b", "a") given an empty frame given an empty frame, for example, will return a frame specifying that ?x $x is bound to a "a" and ?y $y is bound to b. "b". Trying the match with the same pattern, the same datum, and a frame specifying that ?y $y is bound to a "a" will fail. Trying the match with the same pattern, the same datum, and a frame in which ?y $y is bound to b "b" and ?x $x is unbound will return the given frame augmented by a binding of ?x $x to a."a".
The pattern matcher is all the mechanism that is needed to process simple queries that don't involve rules. For instance, to process the query
Original | JavaScript |
(job ?x (computer programmer)) | job($x, list("computer", "programmer")) |
The testing of patterns against frames is organized through the use of streams. Given a single frame, the matching process runs through the data-base entries one by one. For each data-base entry, the matcher generates either a special symbol indicating that the match has failed or an extension to the frame. The results for all the data-base entries are collected into a stream, which is passed through a filter to weed out the failures. The result is a stream of all the frames that extend the given frame via a match to some assertion in the data base.[1]
In our system, a query takes an input stream of frames and performs the above matching operation for every frame in the stream, as indicated in figure 4.7. figure 4.8. That is, for each frame in the input stream, the query generates a new stream consisting of all extensions to that frame by matches to assertions in the data base. All these streams are then combined to form one huge stream, which contains all possible extensions of every frame in the input stream. This stream is the output of the query.
Original | JavaScript | |
To answer a simple query, we use the query with an input stream consisting of a single empty frame. The resulting output stream contains all extensions to the empty frame (that is, all answers to our query). This stream of frames is then used to generate a stream of copies of the original query pattern with the variables instantiated by the values in each frame, and this is the stream that is finally printed.
The real elegance of the stream-of-frames implementation is evident when we deal with compound queries. The processing of compound queries makes use of the ability of our matcher to demand that a match be consistent with a specified frame. For example, to handle the and of two queries, such as
Original | JavaScript |
(and (can-do-job ?x (computer programmer trainee)) (job ?person ?x)) | and(can_do_job($x, list("computer", "programmer", "trainee")), job($person, $x)) |
Find all people who can do the job of a computer programmer trainee), we first find all entries that match the pattern
Original | JavaScript |
(can-do-job ?x (computer programmer trainee)) | can_do_job($x, list("computer", "programmer", "trainee")) |
Original | JavaScript |
(job ?person ?x) | job($person, $x) |
Original | JavaScript | |
Figure 4.11 Figure 4.12 shows the analogous method for computing the or of two queries as a parallel combination of the two component queries. The input stream of frames is extended separately by each query. The two resulting streams are then merged to produce the final output stream.
Original | JavaScript | |
Even from this high-level description, it is apparent that the processing of compound queries can be slow. For example, since a query may produce more than one output frame for each input frame, and each query in an and gets its input frames from the previous query, an and query could, in the worst case, have to perform a number of matches that is exponential in the number of queries (see exercise 4.87).[2] Though systems for handling only simple queries are quite practical, dealing with complex queries is extremely difficult.[3]
From the stream-of-frames viewpoint, the not of some query acts as a filter that removes all frames for which the query can be satisfied. For instance, given the pattern
Original | JavaScript |
(not (job ?x (computer programmer))) | not(job($x, list("computer", "programmer"))) |
Original | JavaScript |
(and (supervisor ?x ?y) (not (job ?x (computer programmer)))) | and(supervisor($x, $y), not(job($x, list("computer", "programmer")))) |
The lisp-value special form javascript_predicate syntactic form is implemented as a similar filter on frame streams. We use each frame in the stream to instantiate any variables in the pattern, then apply the Lisp JavaScript predicate. We remove from the input stream all frames for which the predicate fails.
In order to handle rules in the query language, we must be able to
find the rules whose conclusions match a given query pattern. Rule
conclusions are like assertions except that they can contain
variables, so we will need a generalization of pattern
matching—called unification—in which both the
pattern
and the datum
may contain variables.
A unifier takes two patterns, each containing constants and variables, and determines whether it is possible to assign values to the variables that will make the two patterns equal. If so, it returns a frame containing these bindings. For example, unifying (?x a ?y) list($x, "a", $y) and (?y ?z a) list($y, $z, "a") will specify a frame in which ?x, $x, ?y, $y, and ?z $z must all be bound to a. "a". On the other hand, unifying (?x ?y a) list($x, $y, "a") and (?x b ?y) list($x, "b", $y) will fail, because there is no value for ?y $y that can make the two patterns equal. (For the second elements of the patterns to be equal, ?y $y would have to be b; "b"; however, for the third elements to be equal, ?y $y would have to be a.) "a".) The unifier used in the query system, like the pattern matcher, takes a frame as input and performs unifications that are consistent with this frame.
The unification algorithm is the most technically difficult part of the query system. With complex patterns, performing unification may seem to require deduction. To unify (?x ?x) list($x, $x) and ((a ?y c) (a b ?z)), list(list("a", $y, "c"), list("a", "b", $z)) for example, the algorithm must infer that ?x $x should be (a b c), list("a", "b", "c"), ?y $y should be b, "b", and ?z $z should be c. "c". We may think of this process as solving a set of equations among the pattern components. In general, these are simultaneous equations, which may require substantial manipulation to solve.[5] For example, unifying (?x ?x) list($x, $x) and ((a ?y c) (a b ?z)) list(list("a", $y, "c"), list("a", "b", $z)) may be thought of as specifying the simultaneous equations
Original | JavaScript | |
\[\begin{array}{lll} \texttt{?x} & = & \texttt{(a ?y c)} \\ \texttt{?x} & = & \texttt{(a b ?z)} \end{array}\] | \[\begin{array}{lll} \texttt{\$x} & = & \texttt{list("a", \$y, "c")} \\ \texttt{\$x} & = & \texttt{list("a", "b", \$z)} \\ \end{array}\] |
Original | JavaScript | |
\[ (a ?y c) = (a b ?z) \] | \[\begin{array}{lll} \texttt{list("a", \$y, "c")} & = & \texttt{list("a", "b", \$z)} \end{array}\] |
Original | JavaScript | |
\[ \texttt{a} = \texttt{a},\ \texttt{?y} = \texttt{b},\ \texttt{c} = \texttt{?z} \] | \[\begin{array}{lllllll} \texttt{"a"} & = & \texttt{"a"},\qquad \texttt{\$y} & = & \texttt{"b"},\qquad \texttt{"c"} & = & \texttt{\$z} \end{array}\] |
Original | JavaScript | |
\[\begin{array}{lll} \texttt{?x} & = & \texttt{(a b c)} \end{array}\] | \[\begin{array}{lll} \texttt{\$x} & = & \texttt{list("a", "b", "c")} \end{array}\] |
In a successful pattern match, all pattern variables become bound, and the values to which they are bound contain only constants. This is also true of all the examples of unification we have seen so far. In general, however, a successful unification may not completely determine the variable values; some variables may remain unbound and others may be bound to values that contain variables.
Consider the unification of (?x a) list($x, "a") and ((b ?y) ?z). list(list("b", $y), $z). We can deduce that ?x $x $=$ (b ?y) list("b", $y) and a "a" $=$ ?z, $z, but we cannot further solve for ?x $x or ?y. $y. The unification doesn't fail, since it is certainly possible to make the two patterns equal by assigning values to ?x $x and ?y. $y. Since this match in no way restricts the values ?y $y can take on, no binding for ?y $y is put into the result frame. The match does, however, restrict the value of ?x. $x. Whatever value ?y $y has, ?x $x must be (b ?y). list("b", $y). A binding of ?x $x to the pattern (b ?y) list("b", $y) is thus put into the frame. If a value for ?y $y is later determined and added to the frame (by a pattern match or unification that is required to be consistent with this frame), the previously bound ?x $x will refer to this value.[6]
Unification is the key to the component of the query system that makes inferences from rules. To see how this is accomplished, consider processing a query that involves applying a rule, such as
Original | JavaScript |
(lives-near ?x (Hacker Alyssa P)) | lives_near($x, list("Hacker", "Alyssa", "P")) |
Original | JavaScript |
(rule (lives-near ?person-1 ?person-2) (and (address ?person-1 (?town . ?rest-1)) (address ?person-2 (?town . ?rest-2)) (not (same ?person-1 ?person-2)))) | rule(lives_near($person_1, $person_2), and(address($person_1, pair($town, $rest_1)), address($person_2, list($town, $rest_2)), not(same($person_1, $person_2)))) |
In general, the query evaluator uses the following method to apply a rule when trying to establish a query pattern in a frame that specifies bindings for some of the pattern variables:
Notice how similar this is to the method for applying a procedure function in the eval/apply evaluate/apply evaluator for Lisp: JavaScript:
We saw earlier in this section how to evaluate simple queries in the absence of rules. Now that we have seen how to apply rules, we can describe how to evaluate simple queries by using both rules and assertions.
Given the query pattern and a stream of frames, we produce, for each frame in the input stream, two streams:
Despite the complexity of the underlying matching operations, the system is organized much like an evaluator for any language. The procedure function that coordinates the matching operations is called qeval, evaluate_query, and it plays a role analogous to that of the eval evaluate procedure function for Lisp. JavaScript. Qeval The function evaluate_query takes as inputs a query and a stream of frames. Its output is a stream of frames, corresponding to successful matches to the query pattern, that extend some frame in the input stream, as indicated in figure 4.7. figure 4.8. Like eval, evaluate, qeval evaluate_query classifies the different types of expressions (queries) and dispatches to an appropriate procedure function for each. There is a procedure function for each special syntactic form (and, or, not, and lisp-value) javascript_predicate) and one for simple queries. We use the more verbose name evaluate_query rather than qeval because we were forced to use evaluate instead of eval in section 4.1.
The driver loop, which is analogous to the driver-loop driver_loop procedure function for the other evaluators in this chapter, reads queries from the terminal. typed by the user. For each query, it calls qeval evaluate_query with the query and a stream that consists of a single empty frame. This will produce the stream of all possible matches (all possible extensions to the empty frame). For each frame in the resulting stream, it instantiates the original query using the values of the variables found in the frame. This stream of instantiated queries is then printed.[8]
The driver also checks for the special command assert!, assert, which signals that the input is not a query but rather an assertion or rule to be added to the data base. For instance,
Original | JavaScript |
(assert! (job (Bitdiddle Ben) (computer wizard))) (assert! (rule (wheel ?person) (and (supervisor ?middle-manager ?person) (supervisor ?x ?middle-manager)))) | assert(job(list("Bitdiddle", "Ben"), list("computer", "wizard"))) assert(rule(wheel($person), and(supervisor($middle_manager, $person), supervisor($x, $middle_manager)))) |