L³C

Implementação de
Analisadores Gramaticais por
Deslocamento e Redução (AGDR)

Sobre a análise gramatical por deslocamento e redução, consultar agdr. (Como estou usando a mesma sigla -- AGDR -- tanto para a análise quanto para o analisador gramatical por deslocamento e redução, convém lembrar que a diferença pode ser estabelecida, já que "o AGDR" é o analisador e "a AGDR" é a análise.)

De Covington

Em seu livro, Covington apresenta uma implementação em Prolog de um AGDR em Prolog. Apesar de não ser um AGDR clássico, porque não necessita de um oráculo, não apresentando portanto conflitos de deslocamento-redução ou redução-redução, esta implementação não entre em regressão infinita com regras de estrutura sintagmática com recursão à esquerda. O programa que executa a análise é o seguinte (Covington 1994: 159):

% Bottom-up shift-reduce parser

% parse(+S, ?Result)
%  parses input string S, where Result
%  is list of categories to which it reduces.

parse(S, Result) :-
  shift_reduce(S, [], Result).

% shift_reduce(+S, +Stack,
?Result)
%  parses input string S, where Stack is
%  list of categories parsed so far.

shift_reduce(S, Stack, Result) :-
  shift(Stack, S, NewStack, S1),     % fails if S = []
  reduce(NewStack, ReducedStack),
  shift_reduce(S1, ReducedStack, Result).

shift_reduce([], Result, Result).

% shift(+Stack, +S, -NewStack, -NewS)
%  shifts first element from S onto Stack.

shift(X, [H|Y], [H|X], Y).

% reduce(+Stack, -ReducedStack)
%  repeatedly reduces beginning of Stack
%  to form fewer, larger constituents.

reduce(Stack, ReducedStack) :-
  brule(Stack, Stack2),
  reduce(Stack2, ReducedStack).

reduce(Stack, Stack).

% Phrase structure rules

brule([vp, np | X], [s | X]).
brule([n, d | X], [np | X]).
brule([np, v | X],  [vp | X]).

brule([Word | X], [Cat | X]) :-
  word(Cat, Word).

% Lexicon

word(d, the).

word(n, dog).       word(n, dogs).
word(n, elephant).  word(n, elephants).

word(v, chase).     word(v, chases).
word(v, see).       word(v, sees).

Duas Pequenas Revisões no AGDR de Convington

Ao executar o programa acima, para uma sentença como "the dog sees the elephant" (com ?- parse([the, dog, sees, the, elephant], Result).), por exemplo, o analisador efetivamente chega a uma resposta adequada: Result = [s]. No entanto, se solicitarmos as respostas alternativas que o analisador ainda é capaz de oferecer, vamos perceber que ele responde com um grande número de soluções completamente inadequadas, como Result = [vp, np], Result = [np, v, np], Result = [n, d, v, np], ... desfazendo passo-a-passo as operações de redução.

Isso se deve ao fato de que uma parte importante da condição de término da análise gramatical por deslocamento e redução não tenha sido incluída no analisador: a exigência de que a análise só tenha sido bem-sucedida se a pilha contiver um único símbolo categorial, e não apenas que a fila tenha sido esgotada. A inclusão desta condição é muito simples: ao invés da cláusula shift_reduce([], Result, Result)., bastaria escrever shift_reduce([], [Result], Result)., pois a simples exigência de que a lista que representa a pilha contivesse um único elemento seria suficiente.

Uma segunda revisão proposta aqui não se deve tanto a uma imprecisão da implementação, mas antes a uma pequena divergência de concepção entre a perspectiva lingüística e a computacional sobre o analisador gramatical por deslocamento e redução, apresentada em agdr: revisão. Para implementar esta concepção mais lingüisticamente motivada, a operação de reconhecimento lexical ao invés de ser feita durante a redução (brule([Word | X], [Cat | X]) :- word(Cat, Word).), deve ser feita no deslocamento (shift(Stack, [Word|Words], [Category|Stack], Words) :- word(Category, Word).).

A nova versão revista do mesmo programa seria:

% Bottom-up shift-reduce parser

% parse(+S, ?Result)
%  parses input string S, where Result
%  is list of categories to which it reduces.

parse(S, Result) :-
  shift_reduce(S, [], Result).

% shift_reduce(+S, +Stack,
?Result)
%  parses input string S, where Stack is
%  list of categories parsed so far.

shift_reduce(S, Stack, Result) :-
  shift(Stack, S, NewStack, S1),     % fails if S = []
  reduce(NewStack, ReducedStack),
  shift_reduce(S1, ReducedStack, Result).

shift_reduce([], [Result], Result).

% shift(+Stack, +S, -NewStack, -NewS)
%  shifts first element from S onto Stack.

shift(Stack, [Word|Words], [Category|Stack], Words) :-
  word(Category, Word).

% reduce(+Stack, -ReducedStack)
%  repeatedly reduces beginning of Stack
%  to form fewer, larger constituents.

reduce(Stack, ReducedStack) :-
  brule(Stack, Stack2),
  reduce(Stack2, ReducedStack).

reduce(Stack, Stack).

% Phrase structure rules

brule([vp, np | X], [s | X]).
brule([n, d | X], [np | X]).
brule([np, v | X],  [vp | X]).

% Lexicon

word(d, the).

word(n, dog).       word(n, dogs).
word(n, elephant).  word(n, elephants).

word(v, chase).     word(v, chases).
word(v, see).       word(v, sees).

Adaptação do AGDR revisto de Convington para DCG e Português

O AGDR acima pode ser facilmente transposto para DCG, reposicionando devidamente os argumentos que controlam a fila com a expressão, e algumas vezes introduzindo a variável que falta para fazer a diferença de lista. Abaixo apresenta-se esta transposição, adaptando-se ainda para o português, tanto as próprias definições dos predicados que executam a análise, quanto a língua da expressão a ser analisada.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%   Analisador gramatical    %
% por deslocamento e redução %
%         em DCG             %
%     para português         %
%  baseado no de Covington   %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% analisa(+Expressão, ?Resultado)
%  inicia o processo de análise da Expressão,
%  com a pilha vazia,
%  de forma que em Resultado apareça
%  a lista de categorias a que ela se reduz
analisa(Expressão, Resultado) :-
  analisa([], Resultado, Expressão, []).

% analisa(+Expressão, +Pilha, ?Resultado)
%  analisa a Expressão, sendo que a Pilha
% contém a lista das categorias já analisadas
analisa(Pilha, Resultado) -->
  desloca(Pilha, NovaPilha),                 % falha se Expressão = []
  {reduz(NovaPilha, PilhaReduzida)},
  analisa(PilhaReduzida, Resultado).
analisa([Resultado], Resultado) --> [].

% desloca(+Pilha, +Expressão, -NovaPilha, -RestoDaExpressão)
%  desloca a categoria da próxima Palavra para a NovaPilha
desloca(Pilha, [Categoria|Pilha]) --> [Palavra],
  {palavra(Palavra, Categoria)}.

% reduz(+Pilha, -PilhaReduzida)
%  reduz repetidamente o topo da Pilha
%  para formar constituintes constituintes maiores
reduz(Pilha, PilhaReduzida) :-
  regra_invertida(Pilha, PilhaIntermediária),
  reduz(PilhaIntermediária, PilhaReduzida).
reduz(Pilha, Pilha).

% regra_invertida(+LDR, ?LER)
%  uma regra de estrutura sintagmática como A --> B C
%  é representada como "regra_invertida([b, c | Resto], [a | Resto])."
regra_invertida([sv, sn | Resto], [s | Resto]).     % S --> SN SV
regra_invertida([n, det | Resto], [sn | Resto]).    % Sn --> Det N
regra_invertida([sn, v | Resto],  [sv | Resto]).    % SV --> V SN

% Léxico
palavra(o, det).
palavra(menino, n).
palavra(bolo, n).
palavra(comeu, v).

De Matthews

Há uma outra proposta de implementação em Prolog, um pouco diferente da de Covington, feita por Matthews. A proposta (Matthews 1998: 219-220) é extremamente simples, e tem o seguinte formato:

:- op(1200, xfx, ==>).

parse(Parse, String) :-
   parse([], Parse, String - []).

parse([s(Parse)], Parse, [] -[]).
parse(Stack, Parse, [Word|String] - String1) :-
   word(Word, Cat),
   parse([Cat|Stack], Parse, String - String1).
parse([Cat2, Cat1|Stack], Parse, String - String1) :-
   (Cat ==> [Cat1, Cat2]),
   parse([Cat|Stack], Parse, String - String1).

s(s(NP, VP)) ==> [np(NP), vp(VP)].
np(np(Det, N)) ==> [det(Det), n(N)].
vp(vp(V, NP)) ==> [v(V), np(NP)].

word(the, det(det(the))).
word(snow, n(n(snow))).
word(road, n(n(road))).
word(blocked, v(v(blocked))).


Nesta implementação, como na do Covington acima, o AGDR não enfrenta nunca nenhum tipo de conflito; mas, ao contrário do outro, o deslocamento tem prioridade absoluta em relação à redução. Mas ainda ao contrário da implementação de Covington, nesta já se encontra a compartimentação entre as expressões e as categorias lingüísticas propostas acima.

Assim, uma conseqüência desta implementação é que a análise sintática só começa efetivamente depois de toda a análise lexical ter sido executada (na de Covington, a relação entre o processamento sintático e o lexical era incremental, e não estritamente serial como neste). Contudo, isso poderia ser facilmente resolvido invertendo a ordem entre a primeira e a segunda cláusula da definição do predicado parse/2. Essa inversão resolveria também a questão da recursividade interna das reduções, já que na versão original só poderia ocorrer duas reduções consecutivas depois que não houvesse mais nenhum deslocamento possível.

Além disso, ainda como no AGDR de Covington, aqui também não teria sido necessário exigir na cláusula de término (parse([s(Parse)], Parse, [] -[]).) que a expressão analisada seja necessariamente uma sentença; para garantir que ela seja uma expressão totalmente conexa, basta impor que o primeiro argumento do predicado nesta cláusula seja uma lista com apenas um elemento.

Revisão e adaptação do AGDR de Matthews para DCG e português

Como o AGDR de Matthews já recorre à técnica de diferença de lista, sua adaptação para DCG é imediata. Apresentaremos abaixo, então, esta adaptação, adotando as pequenas adaptações propostas acima, traduzindo os predicados para o português e adotando o português também como língua da gramática.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%   Analisador gramatical    %
% por deslocamento e redução %
%         em DCG             %
%      para português        %
%   baseado no de Matthews   %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% Definição do operador da regra gramatical
:- op(1200, xfx, ==>).

% inicialização
analisa(Análise, Expressão) :-
   analisa([], Análise, Expressão, []).

% condição de término
analisa([
Análise], Resultado) --> [],
    {Análise =.. [_, Resultado]}.
% redução
analisa([SubAnálise2, SubAnálise1|Pilha], NovaPilha) -->
   {Análise ==> [SubAnálise1, SubAnálise2]},
   analisa([Análise|Pilha], NovaPilha).
% deslocamento
analisa(Pilha, NovaPilha) -->
   palavra(Análise),
   analisa([Análise|Pilha], NovaPilha).

% Gramática
s(s(SN, SV)) ==> [sn(SN), sv(SV)].

sn(sn(Det, N)) ==> [det(Det), n(N)].
sv(sv(V, SN)) ==> [v(V), sn(SN)].

% Léxico
palavra(det(det(o))) --> [o].

palavra(n(n(bolo))) --> [bolo].
palavra(n(n(menino))) --> [menino].
palavra(v(v(comeu))) --> [comeu].