14.3 Putting it in Prolog

The first thing we have to do is to decide how we are going to represent the feature based grammar. Once we have done that we will change the active chart parser active_chart_bottomup.pl so that it can handle grammars of this format.

14.3.1 Feature-based Grammars in Prolog

Let's start with the lexicon. A lexical entry for the word robber, for instance, looked like this until now:


Now, we want to have feature structures instead of atomic category symbols. Using the Prolog representation of feature structures that we introduced in the previous section, we want [cat:n,|_] instead of n. So, we will write lexical entry as follows

lex(robber,N) :- N = [cat:n,|_].

Of course, we could also have written


But when you start adding more features lexical entries of the first format might be more readable. It is very easy now to add the information that robber is also singular:

lex(robber,N) :- N = [cat:n,num:sg|_].

Here is another example: the lexical entry for the pronoun him.

lex(him,N) :- N = [cat:pro,num:sg,case:acc|_].

How about the non-lexical rules of the grammar? So far, we have written them as

s ---> [np,vp].

Again we want to replace the atomic non-terminal symbols s, np, and vp by the feature structures [cat:s|_], [cat:np|_], and [cat:vp|_]. And that's what we are going to do. But, again, to make the code more readable we will not write

[cat:s|_] ---> [[cat:np|_], [cat:vp|_]].


S ---> [NP,VP] :-
    S = [cat:s|_],
    NP = [cat:np|_],
    VP = [cat:vp|_].

If we add the requirement that the np must be nominative we get

S ---> [NP,VP] :-
    S = [cat:s|_],
    NP = [cat:np, case:nom|_],
    VP = [cat:vp|_].

And if we further want to make sure that the NP and the VP agree in number, the rule looks like this:

S ---> [NP,VP] :-
    S = [cat:s|_],
    NP = [cat:np, case:nom, num:NUM|_],
    VP = [cat:vp, num:NUM|_].

14.3.2 Parsing Feature-based Grammars in Prolog

In this lecture we want to change the code in active_chart_bottomup.pl so that it will work with feature based grammars in the format that we just introduced. Now, what do we have to change for that? We already saw that the general structure of the general algorithm didn't change at all. Similarly, the general structure of the implementation in active_chart_bottomup.pl won't change. In fact, we can reuse most of the code. We only have to change those places where we access individual non-terminal symbols. I.e., we have to change

  1. apply_fundamental_rule/2 (the fundamental rule),

  2. predict_new_arcs_bottomup/2 (making hyptheses),

  3. the place in process_agenda/1 where we check whether we should add the arc to the agenda,

  4. and the place in active_chart_recognize/1 where we have been successfull.

These are the only places where we access individual non-terminals.

Let's look at apply_fundamental_rule. Here is the old version. It applies the fundamental rule to the first argument and returns all arcs that can be built that way in a list. It uses findall/3 to collect all solutions. We can apply the fundamental rule if we can have a passive arc and an active arc where the symbol that the active arc is looking for next is the same as the symbol that the passive arc is providing.

%%% apply_fundamental_rule(+arc, -list of arcs)
%%% We have an active arc; we are looking for a passive one that
%%% follows it.
apply_fundamental_rule(arc(I, J, Cat, Done, [SubCat|SubCats]), NewArcs) :-
        findall(arc(I, K, Cat, [SubCat|Done], SubCats),
                arc(J, K, SubCat, _, []),
%%% We have a passive arc; we are looking for an active one that
%%% precedes it.
apply_fundamental_rule(arc(J, K, Cat, _, []), NewArcs) :-
        findall(arc(I, K, SuperCat, [Cat|Done], Cats),
                arc(I, J, SuperCat, Done, [Cat|Cats]),

Here is the new version of apply_fundamental_rule. The symbols that the active arc is looking for and the passive arc is providing now don't have to be the same any more, but we want them to unify. unify_silent is the same unification predicate that we saw in the last chapter, except that it can handle empty feature structures properly and doesn't write any output to the screen but returns the result of unification in its third argument.

%%% apply_fundamental_rule(+arc, -list of arcs)
apply_fundamental_rule(arc(I, J, Cat, Done, [CatNeeded|RestNeeded]), NewArcs) :-
        findall(arc(I, K, Cat, [CatUnified|Done], RestNeeded),
                (arc(J, K, CatFound, _, []),
apply_fundamental_rule(arc(J, K, CatFound, _, []), NewArcs) :-
        findall(arc(I, K, SuperCat, [CatUnified|Done], RestNeeded),
                (arc(I, J, SuperCat, Done, [CatNeeded|RestNeeded]),

The changes that we have to make to predict_new_arcs_bottomup are pretty much of the same nature. Instead of demanding that the rules used for new hypotheses have as first symbol on the right hand side a symbol which is identical to the symbol provided by the passive arc, we require that these two feature structures unify. Compare the old version:

%%% predict_new_arcs_bottomup(+arc, -list of arcs)
predict_new_arcs_bottomup(arc(J, _, Cat, _, []), NewArcs) :-
        findall(arc(J, J, SuperCat, [], [Cat|Cats]),
                SuperCat ---> [Cat|Cats],

and the new version:

%%% predict_new_arcs_bottomup(+arc, -list of arcs)
predict_new_arcs_bottomup(arc(J, _, CatFound, _, []), NewArcs) :-
        findall(arc(J, J, SuperCat, [], [CatUnified|Cats]),
                (SuperCat ---> [CatNeeded|Cats],

Most of process_agenda stays the same. But we have to define a new predicate subsuming_edge_in_chart that checks the subsumptions conditions determining whether we add an arc or throw it away.

%%% process_agenda(+agenda)
process_agenda([Arc | Agenda]) :-
        %%% CHANGE: We add the Arc only if there is no subsuming edge
        %%% already in the chart.
        %%% Changed from: \+ Arc.
        \+ subsuming_edge_in_chart(Arc),
        make_new_arcs_bottomup(Arc, NewArcs),
        append(NewArcs, Agenda, NewAgenda),
process_agenda([_|Agenda]) :-
%%% subsuming_edge_in_chart(+arc)
subsuming_edge_in_chart(arc(Start,End,Cat,Found,ToFind)) :-
        %%% There is an arc in the chart which starts in the same position.
        arc(Start, End, CatX, FoundX, ToFindX),
        %%% The feature structures of this arc in the chart subsume all
        %%% corresponding feature structures of the arc in the argument.
        subsumes(CatX, Cat),
        subsumes_list(FoundX, Found),
%%% subsumes_list(+ list of FS, +list of FS)
%%% The feature structures of the first list subsume the corresponding
%%% feature structures of the second list.
subsumes_list([],[]) :- !.
subsumes_list([H1|T1],[H2|T2]) :-

And finally, we have to adapt active_chart_recognize. Only the last line of the old version is affected. We have to make sure that there is a passive arc in the chart that spans the whole sentence and has recognized a constituent that has the category s. We use the predicate val/4, which we introduced in the last chapter, to check whether the features structure on the left hand side of the rule (with which the passive arc is labeled) contains the attribute value pair cat:s.

%%% active_chart_recognize(+sentence)
active_chart_recognize(Input) :-
        initialize_chart_bottomup(Input, 0),
        length(Input, N),
        %%% CHANGEd from arc(0,N,s,_,[])
        arc(0, N, Cat, _, []),

Patrick Blackburn and Kristina Striegnitz
Version 1.2.4 (20020829)