In section 4.4.4 we will present an implementation of the query interpreter as a collection of 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.75). 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 function that classifies queries in a manner analogous to the way 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 datum list(list("a", "b"), "c", list("a", "b")) matches the pattern list($x, "c", $x) with the pattern variable $x bound to list("a", "b"). The same data list matches the pattern list($x, $y, $z) with $x and $z both bound to list("a", "b") and $y bound to "c". It also matches the pattern list(list($x, $y), "c", list($x, $y)) with $x bound to "a" and $y bound to "b". However, it does not match the pattern list($x, "a", $y), since that pattern specifies a list whose second element is the 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.
Using the pattern list($x, $y, $x) to match list("a", "b", "a") given an empty frame, for example, will return a frame specifying that $x is bound to "a" and $y is bound to "b". Trying the match with the same pattern, the same datum, and a frame specifying that $y is bound to "a" will fail. Trying the match with the same pattern, the same datum, and a frame in which $y is bound to "b" and $x is unbound will return the given frame augmented by a binding of $x to "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
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.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.
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
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
can_do_job($x, list("computer", "programmer", "trainee"))job($person, $x)
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.
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.73).[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
not(job($x, list("computer", "programmer")))
and(supervisor($x, $y),
not(job($x, list("computer", "programmer"))))The 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 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 list($x, "a", $y) and list($y, $z, "a") will specify a frame in which $x, $y, and $z must all be bound to "a". On the other hand, unifying list($x, $y, "a") and list($x, "b", $y) will fail, because there is no value for $y that can make the two patterns equal. (For the second elements of the patterns to be equal, $y would have to be "b"; however, for the third elements to be equal, $y would have to be "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
list($x, $x)
list(list("a", $y, "c"), list("a", "b", $z))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 list($x, "a") and list(list("b", $y), $z). We can deduce that $x $=$ list("b", $y) and "a" $=$ $z, but we cannot further solve for $x or $y. The unification doesn't fail, since it is certainly possible to make the two patterns equal by assigning values to $x and $y. Since this match in no way restricts the values $y can take on, no binding for $y is put into the result frame. The match does, however, restrict the value of $x. Whatever value $y has, $x must be list("b", $y). A binding of $x to the pattern list("b", $y) is thus put into the frame. If a value for $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 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
lives_near($x, list("Hacker", "Alyssa", "P"))
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 function in the evaluate/apply evaluator for 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 function that coordinates the matching operations is called evaluate_query, and it plays a role analogous to that of the evaluate function for JavaScript. 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.8. Like evaluate, evaluate_query classifies the different types of expressions (queries) and dispatches to an appropriate function for each. There is a function for each syntactic form (and, or, not, and javascript_predicate) and one for simple queries.
The driver loop, which is analogous to the driver_loop function for the other evaluators in this chapter, reads queries typed by the user. For each query, it calls 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, which signals that the input is not a query but rather an assertion or rule to be added to the data base. For instance,
assert(job(list("Bitdiddle", "Ben"), list("computer", "wizard")))
assert(rule(wheel($person),
and(supervisor($middle_manager, $person),
supervisor($x, $middle_manager))))