Read Exercise 4.76 ~ Solution ~ Tests
conjoin
doesn’t change much:
(define (conjoin conjuncts frame-stream) (if (empty-conjunction? conjuncts) frame-stream (merge-streams (qeval (first-conjunct conjuncts) frame-stream) (conjoin (rest-conjuncts conjuncts) frame-stream))))
We’ve had plenty of practice handling streams so merge-streams
is fairly easy. The idea is to map a new procedure merge-frames
over the frames in the second stream for each frame in the first stream. Since at any point we could generate a ‘failed frame – they need to be filtered out of the resulting merged, flattened stream.
(define (merge-streams left right) (stream-flatmap (λ (left-frame) (stream-filter succeeded? (stream-map (λ (right-frame) (merge-frames left-frame right-frame)) right))) left)) (define (succeeded? frame) (not (failed? frame))) (define (failed? frame) (eq? 'failed frame))
merge-frames
should iterate through the variable bindings in the first frame and using extend-if-possible
with that binding to extend the second frame. I didn’t use map
/filter
so it can bail out early since a 'failed
frame can result at any stage and we’re trying to improve the complexity.
(define (merge-frames left right) (cond ((or (failed? left) (failed? right)) 'failed) ((empty-frame? left) right) (else (let* ((binding (first-binding left)) (var (binding-variable binding)) (val (binding-value binding)) (extension (extend-if-possible var val right))) (if (failed? extension) 'failed (merge-frames (rest-bindings left) extension))))))
I added a few helper procedures for frame bindings.
(define empty-frame? null?) (define first-binding mcar) (define rest-bindings mcdr)
Now for a test spin. The first compound query.
(run-query '(and (job ?person (computer programmer)) (address ?person ?where))) ;; '((and (job (Fect Cy D) (computer programmer)) ;; (address (Fect Cy D) (Cambridge (Ames Street) 3))) ;; (and (job (Hacker Alyssa P) (computer programmer)) ;; (address (Hacker Alyssa P) (Cambridge (Mass Ave) 78))))
More complex.
(run-query '(and (job ?x ?j) (supervisor ?x (Bitdiddle Ben)))) ;; '((and (job (Tweakit Lem E) (computer technician)) ;; (supervisor (Tweakit Lem E) (Bitdiddle Ben))) ;; (and (job (Fect Cy D) (computer programmer)) ;; (supervisor (Fect Cy D) (Bitdiddle Ben))) ;; (and (job (Hacker Alyssa P) (computer programmer)) ;; (supervisor (Hacker Alyssa P) (Bitdiddle Ben)))) (run-query '(and (job ?x ?j) (supervisor ?x (Bitdiddle Ben)) (salary ?x ?amount))) ;; '((and (job (Tweakit Lem E) (computer technician)) ;; (supervisor (Tweakit Lem E) (Bitdiddle Ben)) ;; (salary (Tweakit Lem E) 25000)) ;; (and (job (Fect Cy D) (computer programmer)) ;; (supervisor (Fect Cy D) (Bitdiddle Ben)) ;; (salary (Fect Cy D) 35000)) ;; (and (job (Hacker Alyssa P) (computer programmer)) ;; (supervisor (Hacker Alyssa P) (Bitdiddle Ben)) ;; (salary (Hacker Alyssa P) 40000))) (run-query '(and (job ?x ?j) (supervisor ?x (Bitdiddle Ben)) (salary ?x 40000))) ;; '((and (job (Hacker Alyssa P) (computer programmer)) ;; (supervisor (Hacker Alyssa P) (Bitdiddle Ben)) ;; (salary (Hacker Alyssa P) 40000)))
Looks good, but reviewing Exercise 4.56.
a) the names of all people who are supervised by Ben Bitdiddle, together with their addresses;
(run-query '(and (supervisor ?name (Bitdiddle Ben)) (address ?name ?address))) ;; '((and (supervisor (Tweakit Lem E) (Bitdiddle Ben)) ;; (address (Tweakit Lem E) (Boston (Bay State Road) 22))) ;; (and (supervisor (Fect Cy D) (Bitdiddle Ben)) ;; (address (Fect Cy D) (Cambridge (Ames Street) 3))) ;; (and (supervisor (Hacker Alyssa P) (Bitdiddle Ben)) ;; (address (Hacker Alyssa P) (Cambridge (Mass Ave) 78))))
b. the names of all people who are supervised by Ben Bitdiddle, together with their addresses;
(run-query '(and (salary (Bitdiddle Ben) ?ben-salary) (salary ?name ?salary) (lisp-value < ?salary ?ben-salary))) ; Unknown pat var -- LISP-VALUE (? salary)
Hmmm. For each conjunct qeval
is called with the isolated clause using the initial stream of frames passed into conjoin
. If we try to evaluate (run-query '(lisp-value < ?salary ?ben-salary)))
at the top level i.e. against (singleton-stream '())
of course ?salary
is definitely not bound. We were warned about this in 4.4.3 Is Logic Programming Mathematical Logic? in particular infinite loops and the “Problems with not” caused by the ordering of the conjuncts.
It’s possible to keep the results of qeval
against the first conjunct and then merge that with the stream of recursively calling conjoin with the remaining conjunct and the new stream (but that’s basically the same as the original version of conjoin with the added overhead of merging streams!)
(define (conjoin conjuncts frame-stream) (if (empty-conjunction? conjuncts) frame-stream (let ((first-result (qeval (first-conjunct conjuncts) frame-stream))) (merge-streams first-result (conjoin (rest-conjuncts conjuncts) first-result)))))
I ran a few queries counting the combined calls made to pattern-match
and unify-match
using the 3 conjoin versions:
conjoin1
– the originalconjoin2
– usingmerge-streams
with both streams generated from the same initial stream.conjoin3
– usingmerge-streams
but the second stream is derived from the the first result stream.
Three queries
query 1: (and (job ?x ?j) (outranked-by ?x (Bitdiddle Ben))) query 2: (and (job ?x ?j) (outranked-by ?x (Bitdiddle Ben)) (salary ?x ?amount)) query 3: (and (job ?x ?j) (outranked-by ?x (Bitdiddle Ben)) (lives-near ?x ?who))
Calls made:
query/procedure | query 1 | query 2 | query 3 |
---|---|---|---|
conjoin1 |
3322 | 3610 | 4142 |
conjoin2 |
675 | 1040 | 660 (but gives the wrong result) |
conjoin3 |
3665 | 4023 | 5020 |
Despite being more efficient this version of conjoin results in a lot of previous exercises raising errors due to unbound variables. Still the language semantics have shifted out from under our feet and I think the authors knew all too well we’d run into problems.