324 Lecture Week 11 - Winter 2006 ================================= Horn clauses ------------ Horn clauses - logically, is c <== a_1 & a_2 & ... & a_n - antecedents: conjunction of zero or more conditions (atomic formulae in predicate logic) - consequent: an atomic formula in predicate logic Meaning: "the consequent c is true if the antecedents a_1,...,a_n are all true" Think "(a_1 & a_2 & ... & a_n) implies (c)". Terminology: Horn clause = clause consequent = goal = head antecedents = subgoals = tail Horn clause with no tail = fact Horn clause with tail = rule Writen "c :- a1, ..., an." in Prolog A fact: male(albert). A rule: father(albert,edward) :- male(albert), parent(albert,edward). Variables may appear in antecedents and consequent of a Horn clause: c(X_1, ..., X_n) :- h(X_1, ..., X_n, Y_1, ..., Y_m). means for all values X_1, ..., X_n, the formula c(X_1, ..., X_n) is true if there exist values Y_1, ..., Y_m such that the formula h(X_1, ..., X_n, Y_1, ..., Y_m) is true. Eg. isamother(X) :- female(X), parent(X,Y). ?- isamother(X). X = susan ; X = susan ; No Prolog computation ================== In logic programming, - a program consists of facts and rules - running a program means asking queries - the language tries to find one way (or more) to prove that the query is true - this may have the side effect of freezing variable values - the language determines how to do all of this, not the program - how does the language do it? Using unification, resolution, and backtracking Prolog facts and rules form a knowledge base. Queries ask Prolog to prove a statement is true. Unification is the process Prolog uses to match goals with facts or rules in the knowledge base. Prolog systematically tries to match (unify) the current goal with clauses in the knowledge base. - Prolog might choose blind alleys and find a deadend (end up with a goal that is unsatisfiable), but Prolog knows how to back-up -- BACKTRACKING -- to the last choice point. - Backtracking should look familiar - it's just calling past continuations! Prolog tries all rules in the order they appear in the knowledge base. If a rule does not lead to success, it backs up and tries another. We can arrage all the possible paths of computation into a big tree, which Prolog searches for success. [diagrams in class, and in tutorial notes] Key point: Backtracking always goes to the most recent untried alternative. Prolog searches this tree using DEPTH-FIRST SEARCH, from top to bottom of the knowledge base. Goals and subgoals ------------------ To satisfy a goal, Prolog either matches (unifies with) - a fact, or - a rule. If a fact is matched, Prolog doesn't need to prove anything else, so can go on to the next goal. If a rule is matched, Prolog must prove each of the subgoals before concluding its goal. The subgoals are added to the (front) of the list of goals, and computation continues. Conjoined goals: "and" in Prolog is written with a comma ",". eg. father(X,Y) :- male(X), parent(X,Y). Disjoined goals: "or" is accomplished using multiple rules in Prolog. eg. parent(X,Y) :- father(X,Y). parent(X,Y) :- mother(X,Y). Prolog also uses the semicolon ";" or vertical bar "|" to mean "or". We could rewrite the example as: eg. parent(X,Y) :- father(X,Y) ; mother(X,Y). We generally discourage the use of disjoined goals as the semicolon is so similar to the comma that it's easy to miss or make typographical errors. The ; adds nothing to language, and can be accomplished using multiple rules. Anonymous variables ------------------- An anonymous variable is written as an underscore: _ Can be bound to anything, but never takes a value. Think of it as "matches something, but I don't care what." eg. isamother(X) :- female(X), parent(X,_). We need the binding of X to be a parent of someone, but we don't care who. Negative goals -------------- Old versions of prolog had a "not" predicate - not is a misnomer. The "not" predicate is written as \+ and is pronounced "cannot-prove" or "fail-if". This is VERY DIFFERENT from logical negation! eg. ?- father(albert,edward). Yes ?- \+(father(albert,edward)). No ?- father(edward,albert). No ?- \+ father(edward,albert). Yes We can add negation to a rule: eg. notaparent(X) :- \+ parent(X,_). ?- notaparent(edward). Yes ?- notaparent(albert). No But what happens when we ask about people not in the knowledge base? ?- notaparent(fiona). Yes How does Prolog know this? It can't: it only knows what's in the knowledge base. Prolog knows it can't prove fiona is a parent, so the subgoal parent(fiona,_) fails, and thus the negation succeeds. In the real world, fiona might be a parent! - Prolog makes a CLOSED-WORLD ASSUMPTION: it assumes its knowledge base is complete and nothing else exists that it doesn't know about. - \+ under this assumption is the same thing as "not" - \+ without this assumption is merely a test of whether a query fails Note that \+ never returns a binding for its variables! \+ suceeds if Prolog can't prove its subgoal(s). \+ cannot return a counterexample We can't use \+ to return items that do not satisfy a predicate: eg. \+ father(X,Y). No Prolog finds a single satisfying assignment for father(X,Y), thus \+ father(X,Y) must fail, but no binding of X and Y is possible. IMPORTANT: the order goals appear can make a big difference! eg. sibling(X,Y) :- \+( X = Y ), parent(Z,X), parent(Z,Y). ?- sibling(X,Y). No sibling(X,Y) :- parent(Z,X), parent(Z,Y), \+( X = Y). ?- sibling(X,Y). X = edward Y = alice ; X = alice Y = edward ; ... [why the different behaviour?] Endless computation ------------------- The order of arguments in a relation is important in Prolog. Suppose we want to make a relation like sibling symmetic. We might try adding the rule sibling(X,Y) :- sibling(Y,X). Exercise: why is this a bad idea? [draw the computation tree for a false query] Tracing execution ----------------- Prolog allows you to trace execution. Tracing will print each goal Prolog needs to satisfy, and report bindings/success, failure, and redo attempts. ?- trace. Yes [trace] ?- sibling(X,Y). Call: (7) sibling(_G313, _G314) ? creep Call: (8) parent(_L210, _G313) ? creep Exit: (8) parent(albert, edward) ? creep Call: (8) parent(albert, _G314) ? creep Exit: (8) parent(albert, edward) ? creep Call: (8) edward=edward ? creep Exit: (8) edward=edward ? creep Redo: (8) parent(albert, _G314) ? creep Exit: (8) parent(albert, alice) ? creep Call: (8) edward=alice ? creep Fail: (8) edward=alice ? creep Exit: (7) sibling(edward, alice) ? creep X = edward Y = alice ; Redo: (8) parent(_L210, _G313) ? creep Exit: (8) parent(victoria, edward) ? creep Call: (8) parent(victoria, _G314) ? creep Exit: (8) parent(victoria, edward) ? creep Call: (8) edward=edward ? creep Exit: (8) edward=edward ? creep Redo: (8) parent(victoria, _G314) ? creep Exit: (8) parent(victoria, alice) ? creep Call: (8) edward=alice ? creep Fail: (8) edward=alice ? creep Exit: (7) sibling(edward, alice) ? creep X = edward Y = alice Yes [debug] ?- Manipulating the Knowledge Base =============================== Normally you write the knowledge base as a set of facts and rules in some file (such as family.pl), and tell Prolog to "consult(family)." or "[family].". Recent versions of Prolog consider this data STATIC, and compiles it into a quickly accessible form. Perhaps you want to modify the knowledge base on-the-fly or without having to edit and reconsult the file. You can specify some relations as being DYNAMIC, permitting new facts and rules to be added, or let old facts and rules to be removed from the database. Declaring dynamic predicates (recall the /n specifies arity of the predicate): :- dynamic(parent/2). Asserting new facts about parent: ?- assert(parent(lucy,junior)). Yes Retracting old facts about parent: ?- retract(parent(albert, edward)). Yes With changes being made to the knowledge base, we might want to list everything that is defined. To list everything in the knowledge base: ?- listing. The cut (!) operator ==================== The cut operator, written as an exclamation point in Prolog, allow portions of the computation tree to be pruned, preventing some backtracking for alternatives from occurring. Advice: Unwanted alternatives are a common error in Prolog. Make sure that your predicate rules do the right thing, not only on the first try but also when backtracking for alternatives. The cut operator allows us to commit to a set of choices, probably because know the only solution(s) lie down this path of computation and we want to avoid trying to unify along other paths. Suppose b can be proven by these two rules (b, c, d, ... stand for goals or subgoals): b :- c, d, e, f. b :- g, h. Suppose further that once we manage to prove c and d, we know that this is the only path of computation that can satisfy c and d, and we shouldn't bother trying the other rule to prove b. We can add a cut to tell Prolog to discard any alternatives for proving b, c or d: b :- c, d, !, e, f. b :- g, h. A cut (as a goal) always succeeds, and immediately causes Prolog to forget any alternatives to the current rule and any alternatives to proving anything appearing in the rule before the cut. We are free to examing alternatives to satisfy e and f. Furthermore, if b itself was a subgoal, suppose for proving a, the cut does not prevent examining alternatives for proving a. eg. a :- b. a :- i, j. If the rule with the cut fails (perhaps f is unsatisfiable), Prolog may backtrack to trying to prove a, and try the second a rule. Eg. Try #1: writename(1) :- write('One'). writename(2) :- write('Two'). writename(3) :- write('Three'). writename(X) :- write('Out of range'). /* incorrect */ Need to check range of X [what happens if we ask for next solution?]. Try #2 (better): writename(1) :- write('One'). writename(2) :- write('Two'). writename(3) :- write('Three'). writename(X) :- X > 3, write('Out of range'). writename(X) :- X < 1, write('Out of range'). Code duplication, asking for next solutions attempts unification with additional clauses. Try #3 (better still): writename(1) :- !, write('One'). writename(2) :- !, write('Two'). writename(3) :- !, write('Three'). writename(_) :- write('Out of range'). Note use of anonymous variable. Note also that write creates no alternatives (it just prints its argument). Red cuts vs. Green cuts ----------------------- We can distinguish types of cuts based on whether the set of solutions is changed. Green cut: no possible solutions are cut. Used to eliminate useless backtracking. Eg. adding cuts to rules in Try #2 above. Red cut: prevents Prolog from generating solutions it otherwise yield. Used to Cuts can be tricky if you don't understand EXACTLY what Prolog will do. Do not try to use cuts to make up for sloppy predicate definitions. MORAL: Use cuts SPARINGLY! Make your program CORRECT before trying to make it EFFICIENT. Make sure your code works correctly WITHOUT cuts first before adding them. Writing recursive code in Prolog ================================ Prolog is declarative, but we can't avoid talking about computation. How would we write recursive code or recursive loops in Prolog? Eg. Computing factorials. factorial(0,1) :- !. /* why use a cut here? what gets cut? */ factorial(N,NF) :- N > 0, M is N-1, /* must make a new variable for value of N-1 */ factorial(M,MF), /* the recursive call */ NF is N*MF. Eg. Fibonacci numbers: 1,1,2,3,5,8,... fib(1,1) :- !. fib(2,1) :- !. fib(N,F) :- N > 2, N1 is N-1, fib(N1,F1), N2 is N-2, fib(N2,F2), F is F1 + F2. This definition is elegant, but somewhat inefficient (two recursive calls). [Can you make it more efficient, using one recursive call that remembers the previous Fibonacci number?]