Definite Clause Grammars (DCGs) Example: expr --> atom, [+], expr | atom, [-], expr | atom. atom --> [X], { number(X) } | [abs, ’(’], expr, [’)’]. The predicate phrase/2 is used to parse strings. | ?- phrase(expr, [1, +, 2]). yes | ?- phrase(expr, [1, +, abs, ’(’, 4, +, 5, ’)’]). yes | ?- phrase(expr, [1, +]). no 1 Compiling DCGs to Prolog DCGs can be compiled into Prolog. | ?- listing(expr). expr(A, B) :( atom(A, C), ’C’(C, +, D), expr(D, B) ; atom(A, E), ’C’(E, -, F), expr(F, B) ; atom(A, B) ). yes ’C’/3 is defined as: ’C’([X|Y], X, Y). ’C’/3 is sometimes called connects/3. 2 Compiling DCGs to Prolog, cont. expr(Input, Remainder) :( atom(Input, C), ’C’(C, +, D), expr(D, Remainder) ; atom(Input, E), ’C’(E, -, F), expr(F, Remainder) ; atom(Input, Remainder) ). Remember: ’C’([X|Y], X, Y). 3 Doing something useful expr(E) --> atom(A), [+], expr(E1), { E = A + E1 } | atom(A), [-], expr(E1), { E = A - E1 } | atom(A), {E = A}. atom(A) --> [X], { number(X), A = X } | [abs, ’(’], expr(E), [’)’], { A = abs(E) }. Example: | ?- phrase(expr, [1, +, abs, ’(’, 4, +, 5, ’)’]). E = 1+abs(4+5) 4 Search: Overview • Search problems in Prolog • Depth-first searching • Breadth-first searching and other search orders 5 Search problems Many problems are encodable as search problems. Examples: • Path finding: Can we get from a to f in a graph, given edge(X,Y)? • Planning: How can we get the missionaries and the cannibals across the river without anyone getting eaten? • Logic programming: Is there an SLDderivation, using the available clauses, from our query to an empty goal (i.e. a refutation)? All these share some basic structure. 6 Search problems in logic programming Given a start state, a transition relation, and a goal state (or states), can we reach a goal state from the start state? Components: • A state space (vertices, states of the world, logic programming goals...), represented as Prolog objects. • A transition relation, e.g. edge(X,Y) or move(state(A1,B1),state(A2,B2)). • Start state, goal condition (e.g. goal(State) predicate). • Search procedure. 7 Depth-first example Failed attempt at path finding in a graph: edge(a,b). edge(b,a). edge(a,c). path(X,X). path(X,Z) :- edge(X,Y), path(Y,Z). Loops indefinitely on path(a,c). loop detection. We need 8 Loop detection Solution: Keep a history of visited states. path(X,Y) :- path(X,Y,[X],Answer). path(X,X,Answer,Answer). path(X,Z,Visited,Answer) :edge(X, Y), \+member(Y,Visited), path(Y,Z,[Y|Visited],Answer). This lets us return the actual path, too. Note the search order: ::::- path(b,c). path(b,c,[b], A). path(a,c,[a,b], A). path(c,c,[c,a,b],[c,a,b]). If we’re picky, we can use reverse(Visited,Answer). 9 General depth-first search Suppose that the following predicates are provided: • init(State): Gives the initial state • goal(State): State is a goal state • action(State1,State2): There exists some action (move, edge, etc) taking us from State1 to State2 This is enough to write a general depth-first problem solver (for problems with a finite number of states). Very similar to path of the previous slide. 10 %% df_search(Path): True if Path is a path taken %% from S0 to a goal state df_search(Path) :init(S0), df_search([S0],Path). %% df_search(Partial,Path): True if Partial can %% be extended into a path Path to a goal state df_search([S|Visited], [S|Visited]) :goal(S). df_search([S1|Visited], Path) :action(S1,S2), \+member(S2,[S1|Visited]), df_search([S2,S1|Visited],Path). Example, encoding of the previous problem: init(a). goal(c). action(X,Y) :- edge(X,Y). 11 Maze example Larger example: A small game, a maze with locked doors and coloured keys. State: Current room (a–e), carried keys (blue, green, etc). Start with state(a,[no key]). Actions: Pick up a key (if present), walk through a door (if carrying the right key). Predicates has key(Room, Key) and door(R1, R2, Key) encodes the actual maze. 12 Possible description: has_key(a,blue_key). has_key(d,green_key). has_key(c,red_key). door(a,b,no_key). door(b,d,blue_key). door(a,c,green_key). door(a,e,red_key). init(state(a,[no_key])). goal(state(e,_)). %% Action: walk through door action(state(X,Keys),state(Y,Keys)) :(door(X,Y,Key) ; door(Y,X,Key)), member(Key,Keys). %% Action: pick up key action(state(X,Keys),state(X,[Key|Keys])) :has_key(X,Key), \+member(Key,Keys). 13 Solution: | ?- df_search(P). P = [state(e,[red_key,green_key,blue_key,no_key]), state(a,[red_key,green_key,blue_key,no_key]), state(c,[red_key,green_key,blue_key,no_key]), state(c,[green_key,blue_key,no_key]), state(a,[green_key,blue_key,no_key]), state(b,[green_key,blue_key,no_key]), state(d,[green_key,blue_key,no_key]), state(d,[blue_key,no_key]), state(b,[blue_key,no_key]), state(a,[blue_key,no_key]), state(a,[no_key])] ? 14 Other search orders With depth-first search, Prolog handles the search order for us. With other search strategies, we need to keep track of this ourselves: Maintain a set of all generated candidates (partial paths), and expand them one at a time. Pseudocode: search([Solution|OtherPaths], Solution) :contains_goal(Solution). search([FirstPath|OtherPaths], Solution) :find_all_moves(FirstPath, NewStates), expand(FirstPath, NewStates, NewPaths), insert_paths(NewPaths, OtherPaths, Paths), search(Paths, Solution). The order of insertion controls the search order (e.g., sort by heuristic for A*). 15 Breadth-first search Breadth-first: Place all new candidates last. bf_search([[Goal|Path]|Paths], [Goal|Path]) :goal(Goal). bf_search([[State|Path]|Paths], Solution) :find_all_moves(State, NewStates), expand([State|Path],NewStates, NewPaths), append(Paths, NewPaths, AllPaths), bf_search(AllPaths, Solution). bf_search([[State|Path]|Paths], Solution) :no_moves(State), bf_search(Paths, Solution). % expand([b,a],[c,d], [[c,b,a],[d,b,a]]) expand(L1,L2,L3) :setof([X|L1],member(X,L2),L3). Loop control can be added (for speed, or to avoid infinite answers). 16 Breadth-first search in the maze % bf_maze(Path): Path is a path from start to goal bf_maze(Path) :init(S0), bf_maze([[S0]],Path). % bf_maze(Paths, FinalPath): Paths is a list of % candidate branches, FinalPath is the solution bf_maze([[S|Path]|_],[S|Path]) :- goal(S). bf_maze([[S1|Path]|Partials],FinalPath) :setof(S2,action(S1,S2),NewStates), expand([S1|Path],NewStates,NewPaths), append(Partials,NewPaths,NewPartials), bf_maze(NewPartials,FinalPath). % Remove dead-end branches bf_maze([[S1|_]|Partials],FinalPath) :\+action(S1,_), bf_maze(Partials,FinalPath). % expand([a,b],[c,d], [[c,a,b],[d,a,b]]) expand(L1,L2,L3) :setof([X|L1],member(X,L2),L3). Breadth-first with loop detection is left as an exercise. This variant expands 208 nodes! 17