;;;;;;;;; ;; III ;; ;;;;;;;;; ;; subset ;; ;; Checks if every element of a is a member of b. (defun subset (a b) (cond ((null a) t) ((not (member (car a) b :test #'set-equal)) nil) (t (subset (cdr a) b)))) ;; set-equal ;; ;; Checks if a is a subset of b and b is a subset of a (which implies ;; set equality). (defun set-equal (a b) (if (or (atom a) (atom b)) (equal a b) (and (subset a b) (subset b a)))) ;; pl-resolution-helper ;; ;; This function preforms the bulk of the resolution algorithm. It ;; starts by finding salient clashes between pairs of previously ;; observed clauses and newly generated clauses (clashes are salient ;; if they result in exactly one new clause; otherwise, either there ;; is no clash or all resulting clauses have conjugate literal pairs). ;; Next, it checks to see if any clashes resulted in box, terminating ;; with nil (unsatisfiable) if so, and otherwise checks whether any ;; new clauses were introduced, terminating with t (satisfiable) if ;; not. Finally, pl-resolution recurses, adding clauses generated ;; this step to the list of observed clauses. ;; ;; This is not the fastest implementation of recursion, but it is not ;; the worst. We save time by not considering clashes between the ;; same two clauses more than once, but we potentially waste time by ;; checking for clashes between previously observed clauses and new ;; clauses that immediately resulted from them, as well as between ;; new clauses and themselves. (defun pl-resolution-helper (cs rs) ;; obtain list of new clash results (let ((next (delete-duplicates (loop for c in cs append (loop for r in rs append (let ((res (clash c r))) ;; keep if clashes exactly once; ;; results meaningless otherwise (if (= (length res) 1) res nil)))) ;; clauses are sets; find dups ignoring order of terms :test #'set-equal))) ;; if box or nothing new, we're done (cond ((member nil next) nil) ((subset next cs) t) ;; otherwise, keep checking with new clauses (t (pl-resolution-helper (union next cs :test #'set-equal) next))))) ;; pl-resoultion ;; ;; A simple wrapper for the above. Note that this function expects a ;; list of claueses for input, not a sentence. You should call ;; cnf-list on sentences before calling pl-resolution. (defun pl-resolution (cs) ;; check for box on the off chance that we start with it (if (member nil cs) nil ;; otherwise proceed with normal resolution (pl-resolution-helper cs cs))) ;;;;;;;; ;; IV ;; ;;;;;;;; ;; and-or-not ;; ;; Converts a sentence in PL or FOL such that it only uses logical ;; opperators _and, _or, and _not. (defun and-or-not (s) (cond ((atom s) s) ((equal (car s) '_implies) (let ((a (and-or-not (cadr s))) (b (and-or-not (caddr s)))) `(_or (_not ,a) ,b))) ((equal (car s) '_iff) (let ((a (and-or-not (cadr s))) (b (and-or-not (caddr s)))) `(_or (_and ,a ,b) (_and (_not ,a) (_not ,b))))) (t (cons (car s) (mapcar #'and-or-not (cdr s)))))) ;; deep-not ;; ;; Pushes _not's as deep as possible, eliminating double negatives. (defun deep-not (s) (cond ((atom s) s) ((equal (car s) '_not) (cond ((equal (cadr s) nil) t) ((equal (cadr s) t) nil) ((atom (cadr s)) s) ((equal (caadr s) '_and) (list '_or (deep-not (list '_not (cadadr s))) (deep-not (list '_not (car (cddadr s)))))) ((equal (caadr s) '_or) (list '_and (deep-not (list '_not (cadadr s))) (deep-not (list '_not (car (cddadr s)))))) ((equal (caadr s) '_not) (deep-not (cadadr s))) (t s))) (t (cons (car s) (mapcar #'deep-not (cdr s)))))) ;; or-list ;; ;; Converts a disjunction into a list of its components. (defun or-list (s) (cond ((atom s) (list s)) ((equal (car s) '_not) (list s)) ((equal (car s) '_or) (union (or-list (cadr s)) (or-list (caddr s)) :test #'equal)) (t (list s)))) ;; definite-list-helper ;; ;; This function separates a simple disjunction list into lists for ;; positive and negative terms. (defun definite-list-helper (c &optional neg pos) (cond ((null c) (list neg pos)) ;; c should not contain atoms in our FOL syntax, but handle ;; them appropriately if we find any; an example is nil ((atom (car c)) (definite-list-helper (cdr c) neg (cons (car c) pos))) ;; terms that start with _not go into the list of negatives ((equal (caar c) '_not) (definite-list-helper (cdr c) (cons (cadar c) neg) pos)) ;; everything else is possitive (t (definite-list-helper (cdr c) neg (cons (car c) pos))))) ;; definte-list ;; ;; Calls the above after processing the sentence into a disjunction ;; list. Afterwards, performs a simple sanity check. Note that this ;; sanity check is not thorough; you should look at this function's ;; results for your input to make sure there are no strange ;; constructs. (defun definite-list (s) (let ((res (definite-list-helper (or-list (deep-not (and-or-not s)))))) (if (> (length (cadr res)) 1) (error "~s is not first-order definite" s) res))) ;; subst-list ;; ;; Apply a variable substitution to a sentence in FOL. (defun subst-list (tree pairs) (reduce (lambda (a b) (subst (cadr b) (car b) a)) (cons tree pairs))) ;; next-apart ;; ;; Global variable used by standardize-apart to generate new symbols. (defparameter next-apart 0) ;; standardize-apart-helper ;; ;; Does the work of finding variables and suggesting new symbols. ;; Note that multiple symbols are generated for the same variable if ;; the variable occurs more than once, so the calling function should ;; be sure to remove duplicates before applying the substitution. (defun standardize-apart-helper (p) (cond ((equal p nil) nil) ((equal p t) nil) ;; atoms other than t and nil are variables ((atom p) (list (list p (incf next-apart)))) ;; don't standardize consts ((equal (car p) '_const) nil) ;; recurse otherwise (t (mapcan #'standardize-apart-helper (cdr p))))) ;; standardize-apart ;; ;; Replace existing variables with new symbols to ensure we never have ;; problems with shared variables in unification. (defun standardize-apart (p) ;; the helper only suggests replacements; here we apply them, after ;; removing any duplicate suggestings (not actually necessary with ;; subst-list, but a good percaution) (subst-list p (delete-duplicates (standardize-apart-helper p) :test (lambda (a b) (equal (car a) (car b)))))) ;; p-rename ;; ;; Check if two proven terms differ only by variable names. We ;; accomplish this by attempting to unify the two terms. If variables ;; are only mapped to other variables, and no variable occurs in the ;; map more than once (which would imply that constraint on matching ;; components differs between the terms), then a renaming has been ;; found. (defun p-rename (a b) (let ((pairs (unify a b))) ;; failures to unify are of course not renamings (and (not (equal pairs 'failure)) ;; if we substitute with any constants, not renaming (not (loop for pair in pairs when (not (atom (cadr pair))) return t)) ;; if variable occurs twice, not renaming (see description) (let ((vars (apply #'nconc pairs))) (equal vars (remove-duplicates vars)))))) ;; d-rename ;; ;; Check if a definite clause is a renaming of another definite ;; clause. This is an exceedingly hard problem, as terms in clauses ;; may have different sort orders, and and variables shared between ;; terms must be recognized. Fortunately, just deeming all clauses to ;; not rename each other causes no problems with my implementation of ;; the algorithm, because each added clause is one term shorter than ;; its ancestors, and no clause is ever unified with with the same ;; proven term. ;; ;; Note that this function is not acutally used; I removed calls to it ;; because it was just making things complicated, and never quite ;; worked. (defun d-rename (a b) nil) ;; learn ;; ;; Finds all ways in which a proven term can be unified with portions ;; of a definite clause. New clauses are formed by removing unified ;; terms. Note that only one unified term is removed at a time, so ;; new clauses should be checked again with the same proven term to ;; see if there are more possible unifications. (defun learn (d p) (loop for terms on (car d) append (let ((pairs (unify p (car terms)))) ;; ignore failure (they prove nothing) (if (equal pairs 'failure) nil ;; otherwise, prepare results for collection (list (subst-list (cons (append prev (cdr terms)) (cdr d)) pairs)))) collect (car terms) into prev)) ;; solve ;; ;; Checks if any newly proven terms unify with queries. (defun solve (qs ps) (loop for q in qs append (loop for p in ps append (let ((pairs (unify q p))) ;; ignore failures (they prove nothing) (if (equal pairs 'failure) nil ;; otherwise, prepare results for collection (list (subst-list q pairs))))))) ;; fol-fc-ask-helper ;; ;; Does the bulk of the work for forward-chaining. I've implemented a ;; slightly different algorithm than illustrated on page 282 of ;; Russell/Norvig. The book discribes a BFS algorithm that seeks a ;; single answer for a single query, but the following is an ;; exhaustive DFS-style search that can find many answers for each of ;; many queries. Also, the process of finding unifying terms with ;; unsatisfied clauses is somewhat simplified (the book actually omits ;; any discussion on how this should be done) and more closely ;; resembles resolution than traditional forward-chaining: new clauses ;; are formed by "clashing" known terms against existing clauses' ;; preconditions. ;; ;; The exhaustive nature of this algorithm worsens its exponential ;; time requirements, but its wanton creation of new clauses actually ;; helps reduce repeated work at the expense of memory. There are ;; better ways to implement forward-chaining, but for the purposes of ;; this assignment, they are not worthwhile. Note that this algorithm ;; does speed itself up significantly by only checking for ;; satisfaction of preconditions with newly proven terms. (defun fol-fc-ask-helper (ds kb ps learned qs solved) ;; we're finished if we ever run out of newly proven terms (cond ((null ps) solved) ;; on the other hand, if we run out of definite clauses, start ;; over with the next proven term ((null ds) (fol-fc-ask-helper kb kb (cdr ps) learned qs solved)) ;; otherwise, proceed to check for new deductions (t (let* ((cur-p (standardize-apart (car ps))) ;; find what we can learn (next (learn (car ds) cur-p)) ;; separate this into new partially satisfied ;; definite clauses... (next-ds (loop for d in next when (car d) collect d)) ;; ...and into newly proven terms (next-ps (loop for d in next when (and (null (car d)) (not (member (standardize-apart (caadr d)) learned :test #'p-rename))) collect (caadr d))) ;; finally, check if we've answered any queries (res (solve qs next-ps))) ;; and recurse (fol-fc-ask-helper (append next-ds (cdr ds)) (append next-ds kb) (cons (car ps) (append next-ps (cdr ps))) (append next-ps learned) qs (append res solved)))))) ;; fol-fc-ask ;; ;; Searches for answers for a list of queries, qs, within a ;; knowledge-base, kb, by means of forward-chaining. If more than one ;; answer exists for a particular query, fol-fc-ask returns all of ;; them (in no particular arrangement). kb must be a list of FOL ;; definite clauses in list format, and qs must be a list of FOL ;; terms: ;; ;; kb: ((((LivesIn x r) (LivesIn y r)) ((Roommate x y))) ;; (((LivesIn x r) (Roommate x y)) ((LivesIn y r))) ;; (((LivesIn y r) (Roommate x y)) ((LivesIn x r))) ;; (((Roommate x y)) ((Roomate y x))) ;; (() ((LivesIn (_const Bill) (_const Room201)))) ;; (() ((Roommate (_const Ted) (_const Bill)))) ;; (() ((LivesIn (_const Steve) (_const Room201))))) ;; ;; qs: ((LivesIn (_const Ted) r) ;; (Roommate (_const Bill) x)) ;; ;; Note that sentences in FOL representing definite clauses can be ;; converted into list format through the use of the definite-list ;; function. (defun fol-fc-ask (kb qs) ;; separate the kb into partially satisfied definte clauses... (let* ((init-kb (loop for d in kb when (car d) collect d)) ;; ...and into terms handed to us as true (init-learned (loop for d in kb when (and (null (car d)) (not (member (standardize-apart (caadr d)) res :test #'p-rename))) collect (caadr d) into res finally (return res))) ;; standardize apart the queries just once in the beginning; ;; this is okay because bits of queries have no chance of ;; being substituted into the knowledge base (standard-qs (mapcar #'standardize-apart qs)) ;; find any answers to queries we have from the beginning (init-solved (solve standard-qs init-learned))) ;; and begin (fol-fc-ask-helper init-kb init-kb init-learned init-learned standard-qs init-solved)))