Logic Programming

Logic programming: our last programming paradigm. Rather than telling the computer what to do, here we just tell the computer things that are true, then ask it questions. It does this using a very unintelligent algorithm called unification, which takes two pair structures, possibly with variables in them, and figures out what the variables have to be to make the two patterns the same.

Our query system often trips people up because it's not like Scheme—you don't "call functions", and rule-matching doesn't nest. On the other hand, it still uses lists (and pairs, and dotted-tail notation). But all it does is pattern matching.

In our query system, you use assert! to tell the system new facts, whether they're just plain assertions like (brian likes potstickers) or rules like (rule (same ?x ?x)). Anything else is a query.

Unification

At the core of the query system is an algorithm called unification. Unification is the act of taking in two patterns (pair structures that may or may not have variables) and assigning bindings to the variables so that the structures are equivalent. What you get back is either a list of bindings (variable-value pairs), or some placeholder saying it's impossible to unify these two. The basic algorithm is something like this:

There's also plenty of code to handle variables, but the basic idea is here.

The best way to think about unification is as the query system taking two box-and-pointer diagrams, placing them on top of each other, and seeing where things match up.

In this example, we end up with the following bindings:

?a <=> jordy
?b <=> (jay)
?c <=> brian

Unification can fail whenever we'd try to do something inconsistent. Sometimes that's because we get a pair in one case but a word in another. Other times it's because we get variables that would need two different bindings. Still other times we get two words that don't match.

(brian ?a) and (?a jordy)

The algorithm starts by matching the cars, so ?a gets set to brian. When we get to the cadrs, we have a problem...so the unification fails.

Can you unify (find bindings for) the following pairs of query system patterns?

Writing Query System Rules

If you wanted to ask for the third element of a list, you can't do this:

(car (cdr (cdr ?lst)))  ; bad!

Instead, remember that a query system rule is more like a predicate, checking whether or not a relation holds. To see if ?three is the third element of ?lst, you'd have to do something like this:

(rule (caddr ?lst ?three)
      (and (cdr ?lst ?cdr)
           (cdr ?cdr ?cddr)
           (car ?cddr ?three) ))

This is a lot like a hypothetical Scheme predicate function caddr?:

(define (caddr? lst three)
  (let ((the-cdr (cdr lst)))
    (let ((the-cddr (cdr cdr)))
       (equal? (car the-cddr) three) )))

Note that instead of returning #t or #f, a query system rule just says what has to be true in the correct case. The query system also has much more compact ways of representing relations between pair structures; we could write the caddr rule like this:

(rule (caddr (?car ?cadr ?caddr . ?rest) ?caddr))

In this rule, there are no conditions, but the rule itself will only match if the first "input" is a list that's at least three elements long. There's no need to actually "chain" the operations like we do in the Scheme-predicate-based version.

There's one last thing to note: not.

The query system treats not specially. For a normal query, and for the or special form, the query system will try to come up with any possible bindings that satisfy the query. For and, it will first come up with bindings, then use the remaining queries to throw out any answers that don't match. It's a lot like using filter (and indeed, it uses stream-filter in the implementation).

not is special because theoretically there are an infinite number of possible matches. If I ask (brian likes ?what), the query system only knows (brian likes potstickers) and (brian likes (ice cream)). But if I say (not (brian likes ?what)), I can't really get everything Brian doesn't like. All three of these would be possible answers:

(not (brian likes cheese))
(not (brian likes (the beatles)))
(not (brian likes (1 2 3 4 5 6 7 8 9 10)))

Which are true, false, and somewhat nonsensical, in that order.

Consequently, the query system uses what's called an open-world assumption (you don't have to know that term), which means that it won't say anything it isn't sure about. That means that if you just ask a query with not, you won't get any results back! The same goes for an and with a not as the first clause.

Actually the same thing applies to lisp-value, since the query system has no idea what Scheme procedure you're going to use. But we don't use that very much.

Back to Jordy's home page | Course home page