Logic programming languages such as Prolog have a special connection to natural language analysis. Early researchers in the logic programming field were motivated by the desire to process natural language. Logic programming languages such as Prolog allow grammars for natural language to be expressed in a very simple and natural way while the underlying inference engine allows us to use the grammar for both parsing and generation. Viewed declaratively a grammar for some language L is a static characterization of membership in L. Viewed procedurally, a grammar for some language L is a program for either parsing or generating strings in L.
The following grammar formalisms have the same weak generative capacity:
alpha -> beta1 ...betanalpha is a non-terminal, and the betai's are either terminals or non-terminals. Such a rule is translated into Prolog as,
alpha(P0,Pn) :- beta(P0,P1), . . . beta(Pn-1,Pn).
s
/ \
/ \
/ \
np vp
/ \ / \
d n v np
| | | / \
the cat chased d n
| |
the rat
We can write a grammar to accept the string of words "the cat chased
the rat" with the given analysis as follows:
s --> np vp np --> d n vp --> v np d --> the n --> cat n --> rat v --> chased
s --> np, vp. np --> d, n. vp --> v, np. d --> [the]. n --> [cat]. n --> [rat]. v --> [chased].These grammar clauses are translated into Prolog clauses automatically. How this is done is explained in the following subsections.
Why are difference lists useful? Consider append/3:
append([],Ys,Ys). append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs).append/3 takes time linear in the size of its first argument to append two lists. With difference lists we can write a version of append which appends two difference lists in constant time.
append_dl(Xs-Ys,Ys-Zs,Xs-Zs).How can this work? Let's first convince ourselves that it does, in fact, work. Here's a query posed to Prolog:
| ?- append_dl([1,2,3|As]-As,[4,5,6|Bs]-Bs,Result). As = [4,5,6|Bs], Result = [1,2,3,4,5,6|Bs]-Bs ? ; noThe value of Result is the list [1,2,3,4,5,6] expressed as a general difference list. This is clearly the right answer. How was this answer computed? To answer this question, we have to get our hands dirty and do some unification!
|
| | |
| Stack | Substitution (Z) | Comment |
|
| | |
|
append_dl([1,2,3|As]-As,[4,5,6|Bs]-Bs,Result) = | {} | Inital state. |
| append_dl(Xs-Ys,Ys-Zs,Xs-Zs) | ||
|
| | |
| [1,2,3|As]-As = Xs-Ys, [4,5,6|Bs]-Bs = Ys-Zs, | {} | Pop equation from stack. |
| Result = Xs-Zs | Add 3 equations to stack. | |
|
| | |
| [1,2,3|As]=Xs, As=Ys, | {} | Pop [1,2,3|As]-As = Xs-Ys from stack. |
| [4,5,6|Bs]-Bs = Ys-Zs, Result = Xs-Zs | Same functor "-" and arity, 2. | |
| Add two equations to stack. | ||
|
| | |
| As=Ys, [4,5,6|Bs]-Bs = Ys-Zs, | { Xs=[1,2,3|As] } | Pop [1,2,3|As]=Xs from stack. |
| Result = [1,2,3|As]-Zs | Replace all occurances of Xs by [1,2,3|As] in the stack | |
| Add Xs=[1,2,3|As] to Z. | ||
|
| | |
| [4,5,6|Bs]-Bs = Ys-Zs, | {Xs=[1,2,3|As], | Pop As=Ys from stack. |
| Result = [1,2,3|Ys]-Zs. | As=Ys} | Replace all occurances of As by Ys in the stack. |
| Add As=Xs to Z. | ||
|
| | |
| [4,5,6|Bs] = Ys, Bs = Zs, | {Xs=[1,2,3|As], | Pop [4,5,6|Bs]-Bs = Ys-Zs from stack. |
| Result = [1,2,3|Ys]-Zs | As=Ys} | Same functor "-" and arity, 2. |
| Add two equations to stack. | ||
|
| | |
| Bs = Zs, Result = [1,2,3|[4,5,6|Bs]]-Zs | {Xs=[1,2,3|As], | Pop [4,5,6|Bs] = Ys from stack. |
| As=Ys, | Replace all occurances of Ys by [4,5,6|Bs] in the stack. | |
| Ys=[4,5,6|Bs]} | Add Ys=[4,5,6|Bs] to Z. | |
|
| | |
| Result = [1,2,3|[4,5,6|Zs]]-Zs | {Xs=[1,2,3|As], | Pop Bs = Zs from stack. |
| As=Ys, | Replace all occurances of Bs by Zs in the stack. | |
| Ys=[4,5,6|Bs], | Add Bs=Zs to Z. | |
| Bs=Zs} | ||
|
| | |
| (empty) | {Xs=[1,2,3|As], | Pop Result = [1,2,3|[4,5,6|Zs]]-Zs from stack. |
| As=Ys, | Replace all occurances of Result by [1,2,3|[4,5,6|Zs]]-Zs in the stack. | |
| Ys=[4,5,6|Bs], | Add Result=[1,2,3|[4,5,6|Zs]]-Zs to Z. | |
| Bs=Zs, | ||
| Result=[1,2,3|[4,5,6|Zs]]-Zs} | ||
|
| | |
We're done! The most general unifier is {Xs=[1,2,3|As], As=Ys, Ys=[4,5,6|Bs], Bs=Zs, Result=[1,2,3|[4,5,6|Zs]]-Zs}.
To find the common instance, apply mgu to either term:
append_dl([1,2,3|[4,5,6|Zs]]-[4,5,6|Zs],[4,5,6|Zs]-Zs,[1,2,3|[4,5,6|Zs]]-Zs) =
append_dl([1,2,3,4,5,6|Zs]-[4,5,6|Zs],[4,5,6|Zs]-Zs,[1,2,3,4,5,6|Zs]-Zs).
append_dl([1,2,3|[4,5,6|Zs]]-[4,5,6|Zs],[4,5,6|Zs]-Zs,[1,2,3|[4,5,6|Zs]]-Zs) =
append_dl([1,2,3,4,5,6|Zs]-[4,5,6|Zs],[4,5,6|Zs]-Zs,[1,2,3,4,5,6|Zs]-Zs).
The general format for DCG rules is,
alpha(args) -> beta1(args) ...betan(args)alpha is a non-terminal, and the betai's are either terminals or non-terminals. Such a rule is translated into Prolog as,
alpha(Arg1,...,Argm,P0,Pn) :- beta1(Argi1,1,...,Argi1,k1,P0,P1), . . . betan(Argin,1,...,Argin,kn,Pn-1,Pn).
DCG rules can be extended by adding Prolog goals to them in braces '{' and '}'.
ab --> [a,b]. ab --> [a], ab, [b].
Here's another grammar (non-context free) which generates this language as well.
s --> a(N), b(N).
a(1) --> [a].
a(N) --> [a], a(M), {N is M+1}.
b(1) --> [b].
b(N) --> {N>1, M is N-1}, [b], b(M).
abc --> [a], abc, [b], c1. abc --> []. c1, [b] --> [b], c1. % not a context free rule! c1 --> [c].
Here's a grammar in the style of the second grammar for anbn which generates anbncn.
s --> a(N), b(N), c(N).
a(1) --> [a].
a(N) --> [a], a(M), {N is M+1}.
b(1) --> [b].
b(N) --> {N>1, M is N-1}, [b], b(M).
c(1) --> [c].
c(N) --> {N>1, M is N-1}, [c], c(M).
Prolog simulates nondeterminism through depth-first search with backtracking.
The generate-and-test approach to problem solving is really just an example of nondeterministic thinking. The logic program for the n-queens problem specifies a nondeterministic computation. Prolog simulates nondeterminism by searching for a solution, backtracking where necessary. Prolog doesn't know which computation path will lead to a solution, so it tries all of them until it finds one which works.
If the choice made at a choice point is irrelevant, then it is called don't care nondeterminism. Otherwise, it is called don't know nondeterminism.
Don't care nondeterminism: min/3 with non-mutually exclusive clauses. This is redundancy, and is to be avoided is possible.
A regular grammar is one which is either left linear or right linear.
Productions are of the form alpha -> omegabeta or alpha -> omega, where alpha, beta are nonterminals and omega is a (possibly empty) string of terminals.
Productions are of the form alpha -> betaomega or alpha -> omega, where alpha, beta are nonterminals and omega is a (possibly empty) string of terminals.
accept(Xs) :- initial(Q), accept(Xs,Q). accept([],Q) :- final(Q). accept([X|Xs],Q) :- delta(Q,X,NewQ), accept(Xs,NewQ).
initial(q0). final(q1). delta(q0,b,q1). delta(q0,a,q2). delta(q2,b,q0).
Here's how it works ...
| ?- accept(Xs). Xs = [b] ? ; Xs = [a,b,b] ? ; Xs = [a,b,a,b,b] ? ; Xs = [a,b,a,b,a,b,b] ? yes | ?- accept([a,b,a,b,a,b,b]). yes | ?- accept([a,b,a,b,a,b]). no
initial(q0). final(q1). delta(q0,b,q1). delta(q0,a,q2). delta(q2,b,q0). delta(q2,b,q1).
Nondeterminism: the NFA will accept a string if there is a path which accepts the string. Prolog simulates this nondeterminism using depth-first search and backtracking.
Here's how it works ...
| ?- accept([a,b,a,b,a,b,b]). yes | ?- accept([a,b,a,b,a,b]). yes | ?- accept([a,b,a]). no