========================================================================= * Prolog ========================================================================= * Overview: - used in the area of "logic programming": - making inferences from facts - proving theorems. - Prolog is really the only language used in Logic programming - others are derived from Prolog: Godel, lambda-Prolog, Escher, Curry, Mercury - Important differences between Prolog and most other languages: - You don't run a Prolog Program: You ask questions and the language system answers them (non-procedural) - Logic programs are declarative: the specification of the desired results are written, rather than the way to obtain them - Philosophy: algorithms are hard to write, just write what you want - we'll see it's not this simple - Like Lisp, Syntax is realy simple: only a few keywords and symbols * Terms: - Everything in a prolog program is built from terms - there are 3 kinds of terms: 1. constants: - numbers: 123, -123 - atoms: john, jOHN (starts with a lower case) - they look like variables in other languages but are not: "n" is never equal to anything but just "n" - they are really "symbolic constants" - special atoms: "[]" empty list, "." list concatenation, and others that we will see later... 2. variable: Any name beginning with an upper case letter or an underscore: - example: Alfred - special variable: _ (underscore) sort of a "wildcard" x=a is nonsense X=a has some sense (probably not exactly the one you think though) WARNING: Upercase/Lowercase is a common source of error! 3. compound term: something of the form atom(term,term,term,...) - examples: x(y,z) parent(adam,Child) .(1,[]) They look like function calls, but really are not! It's better to think of these as structured data for now. * Facts: - The prolog language system maintains a collection of facts - Prologs uses these facts to do inference. - The database of facts changes as the system runs - A fact is specified as a term followed by a '.' - Example: % List of parent relationships parent(kim,holly). parent(margaret,kim). parent(herbert,margaret). parent(john,kim). parent(felix,john). parent(albert,felix). - parent is called a predicate of arity 2. - predicates have no intrinsic meaning, but are generally designed to be easily interpreted by the programmer - As a programmer, I will decide that parent(X,Y) means that 'X' is a parent of 'Y' - the predicate is interpreted as a logical relation between X and Y. * How to run prolog: - Interactive mode: 1. Prompts you to type a query 2. You type a query 3. Prolog tries to prove your query 4. Prints out the result (or 'failure') 5. Repeat - A query is a term followed by a '.' - a query looks like a fact, but is just typed at the prompt meaning: "is this fact in your database of facts or be infered from your database of facts". - Prolog prompt: "?-" - How to quite the Prolog interpreter: ?- halt. - "Loading" facts: - you probably want to story your facts in a file, say 'facts.prolog' - 'consult' makes it possible to load facts. - example: ?- consult('facts.prolog'). - Alternatively, you can add a fact to the database by doing: ?- assert(parent(margaret,kim)). - Then on can start asking questions: ?- parent(margaret, john). [This is asking Prolog: "can you prove this?"] No ?- parent(margaret,kim). Yes (If you forget the period then you can type it on the next line) - Any term can appear in a query, including variables. ?- parent(margaret,X). X = kim [press enter if you're satisfied] Yes ?- parent(X,kim). X = margaret ; [press ';' if you want another answer] X=john ; No - Note that in most other languages, a function designed to look up things like this would be less flexible. parentOf() method, childOf() method, loops, etc. ?- parent(X,Y). X=kim Y=holly ; X=margaret Y=kim; ... ?- parent(X,X). No * Unification - The above works with some type of patttern matching that's called "Unification" (Unification is a term from logic theory) - Without going into the theory: 2 terms are said to unify is there is some way of binding their variables so they the 2 terms are identical ?- foo(bar) = foo(bar). Yes - The above shows that the two terms unify. Note that we can ask this from Prolog without "declaring" any of the above. This is because everything is symbolic and this is all just an arbitrary notation that can encode whatever concept. One can type the above right after starting the interpreter. - The more interesting case is them there are variable in there: ?- p(X,dog) = p(cat,Y). X = cat Y = dog Yes ?- a(X,[1,Y],3) = a(2,[1,3],Z). X = 2 Y = 3 Z = 3 Yes - The unification algorithm is interesting, but we don't have time to go into it. Just think of this as pattern matching. * Conjunction - It's not very useful to just ask questions about a single term - A query can be a list of terms separated by commas to form a conjunction (i.e., a logical AND) ?- parent(margaret, X), parent(X, holly). X = kim - Really asks the question: "Can we replace X by something that makes it so that parent(margaret,X) is true AND parent(X,holly) is true", which may mean: "is margaret a grandparent of holly?" ?- parent(X,kim), parent(Y,X), parent(Z,Y). X = john Y = felix Z = albert Yes "does kim have a great-grandparent?" * Rules: - The above gets kind of cumbersome and it would be nice to be able to create queries from other queries. - A rule allows us to do that. For instance, we'd like a rule that says how to prove a great-grandparent relationship - General format: head :- condition, condition, condition. - Meaning: "to prove the head, prove the conditions" - Example: grandparent(GP,GC) :- parent(GP,P), parent (P,GC). "If GP is a parent of P and P a parent of GC, then GP is a grandparent of GC" ?- greatgrandparent(X,holly). X=herbert greatgrandparent(GGP,GGC) :- parent(GGP,GP) , parent (GP, P), parent(P, GGC). - A rule can be added to the "database of facts" to form a "program" that contains two kind of "clauses": facts and rules. - A program is sequence of clauses. - Note that variable P appears twice in the grandparent rule. - What about the scope? - In Prolog: the scope of a variable is in the clause that contains is. foo(P) :- bar(P). There is no connection between P in stuff(P) :- thing(P). the 2 clauses. - No global variables, only local. * Recursive rules: ancestor(X,Y) :- parent(X,Y). [Base case] ancestor(X,Y) :- [Recursive case] parent(Z,Y), ancestor(X,Z). - This is one way to do disjunctions (logical OR) - have multiple clauses stuff(X) :- foo(X). stuff(X) :- bar(X). stuff(X) is true if foo(X) is true OR bar(X) is true - one can also use the ';' operator stuff(X) :- foo(X); bar(X). * Decision process of Prolog ancestor(felix,holly)? / \ parent(felix,holly) parent(Z,holly) NO ancestor(felix,Z) | | Z = kim (by fact) | ancestor(felix,kim) / \ parent(felix,kim) parent(Z',kim) NO ancestor(felix,Z') ----------| | | Z'=john Z'=margaret | | | ancestor(felix,john) ancestor(felix,margaret) | / \ parent(felix,john) parent(felix,margaret) parent(Z'',margaret) YES NO ancestor(felix,Z'') | Z'' = herbert | | ancestor(felix,herbet) / | parent(felix,herbert) parent(Z''',herbert) NO NO - The process by which Prolog goes back up the tree is called "backtracking" * Trace mode in prolog: shows the tree ?- trace. - the subsequent query is traced. - use the on-line help on the ACS Prolog interpreter * Order of clauses and terms: the order matters - Reason: facts are examined in order conditions are examined left-to-right - In the above example: 1. If we had gone first into the branch Z'=john rather than to Z'=margaret, then we would have found things faster 2. If we had swapped the two conjunctions in the recursive rule we would have done a different tree - Useful for performance - RULES of thumbs: Try simple things first! * In fact, there are cases in which a program may not even WORK depending on the order: - a simple infinite loop ancestor(X,Y) :- ancestor(X,Z), parent(Z,Y). ancestor(X,Y) :- parent(X,Y). ?- ancestor(X,holly). ERROR: Out of local stack (Just like a left-recursive production rule and recursive-descent parsing) - the sibling example sibling(X,Y) :- parent(P,X), parent(P,Y) - looks right? NO: sibling(X,Y) will work if X=Y!! sibling(X,Y) :- not(X=Y), parent(P,X), parent(P,Y). - looks right? - NO: first goal: prove not(X=Y). - Will X=Y fail? - Will not fail because the variables are not bound - Try it on the Prolog interpreter 1 ?- X=Y. X = _G183 Y = _G183 Yes - Thus not(X=Y) fails. - Thus everything fails. sibling(X,Y) :- parent(P,X), parent(P,Y), not(X=Y). - correct. - This shows a major weakness: You can's just rely on the logical meanings and you sort of need to know how things work. - We'll see that many, many things break down the pure philosophy that says: "Just write what you need logically" * Backtracking - Ordering clauses and goals is a way to somewhat control the search and backtracking process, but it is very limited. - There is something called a "cut" that prevents Prolog from backtracking. - Example: Let's say we're writing a program to compute the following step function: X < 3 phi(X) = 0 3 <= X < 6 phi(X) = 2 6 <= X phi(X) = 4 In Prolog we can implement this with a binary predicate, f(X,Y), which is true if Y is the function value at point X. For instance, f(0,0) is true, f(4,2) is true, but f(2,4) is false. Here is the program: f(X,0) :- X < 3. [rule 1] f(X,2) :- 3 =< X, X < 6. [rule 2] note '=<' f(X,4) :- 6 =< X. [rule 3] There are two sources of inefficiency in this program, that we'll see on one example: ?- f(1,Y), 2 < Y. [find a Y such that Y = f(1) and 2 < Y] [ we can see this is going to fail] what does Prolog do? f(1,Y) 2 < Y ---------- rule 1 / \ \ Y = 0 / \ rule 2 \ rule 3 / | Y = 2 | Y = 4 | | | 1 < 3 3 <= 1 6 <= 1 2 < 0 1 < 6 2 < 4 | 2 < 2 NO | NO 2 < 0 NO There is really no point in trying rule 2 and rule 3 because since X < 3, we know that rule 2 and rule 3 will fail. Basically, the three rules are mututally exclusive. We know that. Prolog doesn't. So, we can "cut" the backtracking by using the '!' operator: f(X,0) :- X < 3, !. f(X,2) :- 3 <= X, X < 6, !. f(X,4) :- 6 <= X. The new execution looks like: f(1,Y) 2 < Y rule 1 / Y = 0 / / | 1 < 3 2 < 0 | CUT | 2 < 0 NO Lessons: cuts can be used to prevent Prolog from going into branches of the search tree that we know, due to our understanding and knowledge of the problem, will not succedd anyway. - There are many more things possible with cuts and using them well is an art. A program with no cuts at all will run orders of magnitude slower than an equivalent program with a few '!' thrown in. * Lists - basics: [] empty list - Two notations: .(1,.(2,.(3,[]))) (sort of looks like a predicate but isn't) [1,2,3] - "car" and "cdr" - symbol '|' separates tail from first element(s): [1|X] unifies with any list that starts with 1 [1,2|X] unifies with any list that starts with 1 and 2 [X|Y] unifies with any non-empty list. - Examples: ?- [X|Y] = [2]. X = 2 Y = [] ?- [X,Y|Z] = [1,2,3]. X = 1 Y = 2 Z = [3] ?- [1|Z] = [X|Y]. Z = _G250 [unbound variable] X = 1 Y = _G250 [same unbound variable] - Build-in predicates: reverse, sort, append, etc... - example: append append(X,Y,Z) is true if Z is the list resulting from appending Y to X. ?- append([1,2],[3,4],Z). Z=[1,2,3,4] Yes ?- append(X,[3,4],[1,2,3,4]). X=[1,2] ?- append(X,Y,[1,2,3]). X = [] Y = [1,2,3] ; X = [1] Y = [2,3] ; X = [1,2] Y = [3] ; X = [1,2,3] Y = [] Yes - How would you write append() in Prolog? append([], B, B). (base case) append([Head | TailA ] , B, [Head | TailC]) :- (recursive case) append(TailA, B, TailC). - First rule: The result of appending any list B to the empty list is list B - Second rule: recursive and much more complex - if the first and the third lists start with the same element, then you can prove the relation if TailC is the concatenation of TailA and B. - VERY DIFFERENT WAY of thinking than imperative languages - Example: append([1],[2],Z) Z unifies to [1,TailC] ---> must prove append([], [2], TailC). ---> true is TailC unifies to [2] ---> therefore: Z = [1,2] - Anonymous variable '_' - Every occurence of '_' is bound independently of every other - Example: tailof([Head|A],A). correct, but introduces a "Singleton variable" which is never used. tailof([_|A],A). is prefered. - How would you write has3orMoreElements? has3orMoreElements([_,_,_|_]). - How would you write hasSame1stAnd5ndElements? hasSame1stAnd5ndElements([X,_,_,_,X|_]). - How would you write isin(X,Y) (true if X is an element of list Y) isin(X,[X|_). isin(X,[_|Y]) :- isin(X,Y) * Numeric computation - Although Prolog is mostly symbolic, there is a need for numeric computation - '=' is the unification operator ?- X = 2+3. X = 2+3 Yes - 'is' evaluates arithmetic expressions before doing unification ?- X is 2+3. X = 5 Yes - When prolog tries to solve an "is" goal it evalutes the second argument and then unifies, as opposed to "=" which just does the unification. ?- Y is X+2, X=1. ERROR: Args are not sufficiently instanciated ?- X=1, Y is X+2. X=1 Y=3 Yes - Again, order of evaluation matters! * Example: mylength(L,X) is true if X is the length of list L mylength([],0). mylength([_|Tail],Len) :- mylength(Tail,TailLen), Len is TailLen +1. ?- mylength([1,2],L). does not unify with mylength([],0) unifies with mylength([_|Tail],Len) with the bindings: Tail = [2] and Len = L now I need to prove the two things: mylength([2],TailLen) and Len is TailLen + 1 can I prove the first one? mylength([2],TailLen) does not unify with mylength([],0) mylength([2],TailLen) unifies with mylength([_|Tail'],Len') with the bindings: Tail' = [] and Len' = TailLen now I need to prove the two conditions: mylength([],TailLen'') and Len' is TailLen'' + 1 can I prove the first one? mylength([],TailLen'') unifies with mylength([],0) with the bindings: TailLen'' = 0 Len' is TailLen'' + 1 then leads to the binding Len' = 1 therefore TailLen is equal to Len', and thus to 1 therefore Len is equal to Len' + 1, and thus to 2 therefore L is equal to Len, and thus to 2 Prolog answers L = 2. * Common technique: using an accumulator - There are cases in which you want to add an argument to a predicate just to keep track of useful information - Example: return a list of all X's such that foo(X) is true. foo(a). foo(b). allfoos(L) :- listallfoos(L,[]). % recursive case listallfoos([X|L],SoFar) :- foo(X), not(isin(X,SoFar)), append(SoFar,[X],NewSoFar), listallfoos(L,NewSoFar). % base case listallfoos([],_). ?- allfoos(A). must prove listalllfoos(A,[]). unifies with listallfoos([X|L],[]) must prove four things: foo(X) not(isin(X,[]) append([],[X],NewSoFar) listallfoos(L,NewSoFar) foo(a) true (X is bound to a) not(isin(a,[])) true append([],[a],NewSoFar) true with NewSoFar=[a] must prove listallfoos(L,[a]) unifies with liastallfoos([Y|L'],[a]) must prove four things: foo(Y) not(isin(Y,[a]). append([a],[Y],NewSoFar') listallfoos(L',NewSoFar') foo(a) true not(isin(a,[a])) false BACKTRACK foo(b) true (Y is bound to b) not(isin(b,[b])) true append([a],[b],NewSoFar') true with NewSoFar' = [a,b] must prove listallfoos(L',[a,b]) unifies with listallfoos([Z|L''],[a,b]) must prove four things: foo(Z) not(isin(Z,[a,b]) one can see that will fail BACKTRACK unifies with listallfoos([],[a,b]). therefore: L' unifies with [] therefore: [Y|L'] unifies with [b] therefore L unifies with [b] therefore [X|L] unifies with [a,b] therefore A unifies with [a,b] therefore allfoos([a,b]) - If you try this code, and hit ';', you'll get multiple answers - Try to figure out why (using the "trace" mode) - Solution: add a cut listallfoos([X|L],SoFar) :- foo(X), not(isin(X,SoFar)), append(SoFar,[X],NewSoFar), listallfoos(L,NewSoFar),!. * Hanoi towers - Goal: Print out instructions to solve the problem - The basic action is to move 1 disk, with printing move(A,B) :- nl, write ('Move topdisk from '), write(A), write(' to '), write(B). - the main predicate is transfer(N,A,B,X): represents "Move N disks from peg A to peg B by using peg X as a helper" - base case: transfer(1,A,B,X) :- move(A,B). Better written as: transfer(1,A,B,_) :- move(A,B). - inductive case: transfer(N,A,B,X) :- transfer the top N-1 disks to X transfer the (bottom) disk from A to B transfer the top N-1 disks from X to B transfer(N,A,B,X) :- M is N-1, transfer(M,A,X,B), move(A,B), transfer(M,X,B,A). ?- transfer(3,peg1,peg3,peg2). Move topdisk from peg1 to peg3 Move topdisk from peg1 to peg2 Move topdisk from peg3 to peg2 Move topdisk from peg1 to peg3 Move topdisk from peg2 to peg1 Move topdisk from peg2 to peg3 Move topdisk from peg1 to peg3 * Advanced database operations via pattern-matching: board(length(9,2,feet),3,[color(top,white),color(bottom,red),color(rail,orange)]). board(length(8,1,feet),3,[color(top,red),color(bottom,red),color(rail,orange)]). board(length(7,1,feet),3,[color(top,blue),color(bottom,red),color(rail,orange)]). board(length(5,4,feet),3,[color(top,purple),color(bottom,red),color(rail,orange)]). ?- board(length(X,Y,feet),_,_). returns the list of board lengths in the database ?- board(_,_,[_,_]). Are there boards with exactly two colors? ?- board(_,_,[_,_|_]). Are there boards with two colors or more * colorExist(Color) is true if Color is on at least one board: colorExists(Color) :- board(_,_,ColorList), % "extract" the color list isInColorList(ColorList,Color). % check that the color is in it isInColorList([color(_,Color)|_],Color). isInColorList([_|Tail],Color) :- isInColorList(Tail,Color). ?- colorExists(orange). Yes ?- colorExists(Color). [ prints all known colors ] * Goat / Farmer/ Wolf / Cabbage (code on Web page) Goat | | Goat eats cabbage if no farmer Wolf | river | Wolf eats goat if no farmer Cabbage | | One one spot on the boat Configure the "state" of the program as a list with the location of the four objects (farmer, wolf, goat, cabbage). There are two locations: West (w) and East (e). initial state: [w,w,w,w] desired state: [e,e,e,e] There are four kinds of moves: with the cabbage, with the goat, with the wolf, with nothing. For instance, [w,w,w,w] "move wolf" [e,e,w,w] We encode this as: move([w,w,w,w],wolf,[e,e,w,w]). (this just says that the state transformation above is true) We could write all the possible moves as facts, but there would be a lot. However, it is clear that when the farmer and wolf move, the goat and the cabbage do not move, so a more general fact is: move([w,w,Goat,Cabbage],wolf,[e,e,Goat,Cabbage]). and move([e,e,Goat,Cabbage],wolf,[w,w,Goat,Cabbage]). therefore, a more general goal is: move([X,X,Goat,Cabbage],wolf,[Y,Y,Goat,Cabbage]) :- change(X,Y) where we have: change(e,w). change(w,e). [enforces that X and Y above cannot be equal to any atom] Now we can just write the whole program: move([X,X,Goat,Cabbage],wolf,[Y,Y,Goat,Cabbage]) :- change(X,Y) move([X,Wolf,X,Cabbage],goat,[Y,Wolf,Y,Cabbage]) :- change(X,Y) move([X,Wolf,Goat,X],cabbage,[Y,Wolf,Goat,Y]) :- change(X,Y) move([X,Wolf,Goat,Cabbage],nothing,[Y,Wolf,Goat,Cabbage]) :- change(X,Y) [***] At this point we have encoded all the possible moves. But there is nothing about moves being safe or unsafe. We need a safe predicate that takes a state as input and is true if the state is sage (nobody eats nobody). "if at least one of the goat or the wolf is on the same bank as the farmer, AND if at least one of the goat or cabbage is on the same bank as the farmer, then we're safe" We define the oneEq(X,Y,Z) predicate that returns true if at least one of Y or Z is equal to X: oneEq(X,X,_). oneEq(X,_,X). then we can have: safe([Farmer,Wolf,Goat,Cabbage]) :- oneEq(Farmer,Goat,Wolf), oneEq(Farmer,Goat,Cabbage). this encodes the logical statement we made above. A solution is defined as a sequence of moves, or recursively as as one move that takes you to a safe configuration from which we can get to the solution: solution([e,e,e,e],[]). solution(State,[FirstMove|OtherMoves]) :- move(State,Move,NextState), safe(NextState), solution(NextState,OtherMoves). The program is complete. Example run: If you just type solution([w,w,w,w],X), we get into an infinite loop as there are infinitely solutions. So: ?- length(X,7), solution([w,w,w,w],X). X = [goat, nothing, wolf, goat, cabbage, nothing, goat] In fact, 7 steps is the shortest solution. ?- length(X,12), solution([w,w,w,w],X). (needs an odd number of moves) No ?- length(X,13423), solution([w,w,w,w],X). [goat, goat, goat, goat, goat, goat, goat, ....., +7 steps] is one of the solutions