Основы программирования на языке Visual Prolog

Парсеры

Разбить на страницы
Показывать лекцию целиком

В настоящей главе изучаются синтаксические анализаторы, или парсеры. Синтаксический анализатор проверяет строку на соответствие грамматике.

Формальная грамматика — это четверка вида $$\langle N, T, S, P \rangle$$, где $$N$$ — множество нетерминальных символов, $$T$$ — множество терминальных символов, $$S$$ — начальный нетерминальный символ, $$P $$— множество правил. Множества $$N$$ и $$T$$ не пересекаются В контекстно-свободной грамматике правила, в форме Бэкуса-Наура (БНФ), имеют вид:

$$x ::= X $$

(используется также обозначение $$x \to X$$), где $$x$$ — нетерминальный символ грамматики, а $$X$$ — последовательность терминальных и нетерминальных символов.

Язык состоит из фраз — последовательностей терминальных символов. Фраза (или слово, предложение, цепочка символов) принадлежит языку, если она может быть выведена с помощью применения правил грамматики из ее начального символа конечное число раз. Применение правила $$x ::= X$$ преобразует слово $$AxB$$ в слово $$AXB$$.

Результатом анализа, или разбора строки является дерево разбора, листьями которого являются терминальные символы грамматики, а остальными вершинами — нетерминальные символы.

11.1. Анализ английских предложений

Рассмотрим английские предложения вида [4]:

Every man that lives loves a woman. John likes Mary.

Грамматику таких предложений можно описать следующим образом:

  • предложение состоит из группы существительного и группы глагола;
  • группа существительного состоит из собственного имени или спецификатора, нарицательного имени и придаточного предложения;
  • придаточное предложение состоит из указательного местоимения и глагольной группы;
  • глагольная группа состоит из непереходного глагола или переходного глагола и группы существительного.
  • Грамматика в БНФ имеет вид:

    sent   ::=  np  vp
    np   ::=  spec1  noun  relcOpt  |  np1
    np1   ::=  pnoun  |  spec2  noun  relcOpt 
    vp   ::=  verb1  |  verb2  np1
    relcOpt ::=  relprn  vp  |  none
    noun   ::=  [man]  |  [woman]  |  [cat]  |  [dog]
    pnoun   ::=  [John]  |  [Mary]
    verb1   ::=  [lives]  |  [sings]  |  [runs]
    verb2   ::=  [loves]  |  [likes]
    spec1   ::=  [every]
    spec2   ::=  [a]
    relprn   ::=  [that]
    

    Множество нетерминальных символов образуют символы из левых частей правил. Терминальные символы заключены в квадратные скобки. Начальным символом грамматики является символ sent.

    Дерево разбора предложения "every man that lives loves a woman" в соответствии с данной грамматикой приведено на рис. 11.1.

    (рис 11.1) Дерево разбора

    В следующей программе строится дерево разбора предложений, удовлетворяющих грамматике. Дерево разбора представляется в виде терма. Домен таких термов определяется рекурсивно (см. программу).

    Первым аргументом предиката parser/4 является нетерминальный символ грамматики. Каждое правило, описывающее этот предикат, соответствует правилу грамматики, указанному в комментарии. На вход предиката также поступает список токенов. Предикат возвращает терм фрагмента дерева разбора и остаток списка токенов.

    Правила грамматики, в правых частях которых отсутствуют нетерминальные символы, реализуются в виде фактов.

        open core, console, string
    
    class predicates      % разбиение на токены
        scan: (string) -> string*.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
    domains      % парсер
        term = sent(term Np, term Vp); np(string, string, term Relc);
            pn(string); vp(string, term Np); relc(string, term Vp); empty.
        nt = sent; np; np1; vp; relc.
    class predicates
        parser: (nt, string*, term [out], string* [out]).
    clauses
    % sent ::= np vp
        parser(sent, L, sent(Np, Vp), Rest):-
            parser(np, L, Np, L1),
            parser(vp, L1, Vp, Rest),
            !.
    
    % np ::= spec1 noun relcOpt
        parser(np, [Spec, Noun | L], np(Spec, Noun, Relc), Rest):-
            spec1(Spec),
            noun(Noun),
            !,
            parser(relc, L, Relc, Rest).
    
    % np ::= np1
        parser(np, L, Np, Rest):- !,
            parser(np1, L, Np, Rest).
    
    % np1 ::= pnoun
        parser(np1, [Name | L], pn(Name1), L):-  
            pnoun(Name1),
            Name = toLowerCase(Name1),
            !.
    
    % np1 ::= spec2 noun relcOpt
        parser(np1, [Spec, Noun | L], np(Spec, Noun, Relc), Rest):-
            spec2(Spec),
            noun(Noun),
            !,
            parser(relc, L, Relc, Rest).
    
    % vp ::= verb1
        parser(vp, [Verb | L], vp(Verb, empty), L):-
            verb1(Verb),
            !.
    
    % vp ::= verb2 np1
        parser(vp, [Verb | L], vp(Verb, Np), Rest):-
            verb2(Verb),
            !,
            parser(np1, L, Np, Rest).
    
    % relcOpt ::= relprn vp
        parser(relc, [Rel | L], relc(Rel, Vp), Rest):-
            relprn(Rel),
            !,
            parser(vp, L, Vp, Rest).
    
    % relcOpt ::= none
        parser(_, L, empty, L).
    
    class facts
        noun: (string).
        pnoun: (string).
        verb1: (string).
        verb2: (string).
        spec1: (string).
        spec2: (string).
        relprn: (string).
    clauses
        noun("man").
        noun("woman").
        noun("cat").
        noun("dog").
    
        pnoun("John").
        pnoun("Mary").
    
        spec1("every").
    
        spec2("a").
    
        verb1("lives").
        verb1("sings").
        verb1("runs").
    
        verb2("loves").
        verb2("likes").
    
        relprn("that").
    
    class predicates      % печать дерева разбора
        print: (term).
        print: (term, charCount).
        f: (charCount, string).
    clauses
        f(N, S):-
            write(string::create(N, "\t"), S), nl.
    
        print(Term):-
            print(Term, 0).
    
        print(sent(Np, Vp), N):-
            f(N, "sent"),
            print(Np, N + 1),
            print(Vp, N + 1).
        print(np(Spec, Noun, Relc), N):-
            f(N, "np"), 
            f(N + 1, "spec"), f(N + 2, Spec),
            f(N + 1, "noun"), f(N + 2, Noun),
            print(Relc, N + 1).
        print(pn(Name), N):-
            f(N, "np"), f(N + 1, Name).
        print(relc(Rel, Vp), N):-
            f(N, "relc"), 
            f(N + 1, "relprn"), f(N + 2, Rel),
            print(Vp, N + 1).
        print(vp(Verb, Np), N):-
            f(N, "vp"), 
            f(N + 1, "verb"), f(N + 2, Verb),
            print(Np, N + 1).
        print(empty, _).
    
        run():-
            Str = "Every man that lives loves a woman.",
            write(Str), nl,
            L = scan(toLowerCase(Str)),
            parser(sent, L, Term, Rest),
            write(Rest), nl,
            write(Term), nl, nl,
            print(Term),
            _ = readLine().
    

    Предикат toLowerCase переводит все символы строки в нижний регистр.

    Упражнение 1. Добавьте в предложения существительные множественного числа. Постройте грамматику и напишите парсер таких предложений.

    11.2. Анализ арифметических выражений

    Рассмотрим арифметические выражения вида:

    -2 + 3 * x; 
    -2^2 * 3 + 2 * 4 / (3 - 1) + (sin(pi/6 + pi/3)) ^ 2 - ln (2 * e^3 - 1); 
    2^3^2 - 3 * (4 + 10^2) * (3 - 10^2); 
    7.
    

    В следующей программе выполняется синтаксический анализ таких выражений. Разбор выполняется в соответствии с праворекурсивной грамматикой, которая имеет следующий вид:

    expr  ::=  addSignOpt  item  items
    item  ::=  deg  potens
    deg  ::=  elem  elems
    elem  ::=  fun  [(]  expr  [)]  |  [(]  expr  [)]  |  [pi]  |  [e]  |  num  |  var
    items  ::=  addSign  item  items  |  none
    potens   ::=  multSign  deg  potens |  none
    elems  ::=  [^]  elem  elems  |  none
    addSignOpt  ::=  addSign  |  none
    addSign   ::=  [+]  |  [-]
    multSign  ::=  [*]  |  [\]
    fun  ::=  [cos]  |  [sin]  |  [exp]  |  [ln]
    

    Сначала программа строит терм дерева разбора. Если выражение не содержит переменных, то по терму вычисляется его значение. Затем выполняется обратное преобразование терма в строку. Предикат parser/4 по входному списку токенов и нетерминальному символу грамматики (expr, item, deg или elem) возвращает подтерм дерева разбора и остаток списка токенов. Предикат parser/5 имеет еще один аргумент — терм, который соответствует первому аргументу бинарной операции (сложения, вычитания, умножения, деления или возведения в степень). Он рекурсивно строит терм для последовательности аргументов этой операции (см. правила для символов items, potens и elems).

    class predicates
        scan: (string) -> string*.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            string::frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
    domains                                                 % парсер
        term = un(string Op, term); bin(string Op, term, term);
            func(string Op, term); var(string); r(real); pi(); e().
        nt = expr; item; items; deg; potens; elem; elems.
    class predicates
        parser: (nt, string*, term [out], string* [out]) determ.
        parser: (nt, string*, term, term [out], string* [out]).
    clauses
        parser(expr, [S | L], Term, Rest):-
            addOp(S),
            !,
            parser(item, L, Term1, L1),
            parser(items, L1, un(S, Term1), Term, Rest).
        parser(expr, L, Term, Rest):-
            parser(item, L, Term1, L1),
            parser(items, L1, Term1, Term, Rest).
        parser(item, L, Term, Rest):-
            parser(deg, L, Term1, L1),
            parser(potens, L1, Term1, Term, Rest).
        parser(deg, L, Term, Rest):-
            parser(elem, L, Term1, L1),
            parser(elems, L1, Term1, Term, Rest).
        parser(elem, [S, "(" | L], func(S, Term), Rest):-
            fun(S),
            !,
            parser(expr, L, Term, L1),
            L1 = [")" | Rest].
        parser(elem, ["(" | L], un("()", Term), Rest):- !,
            parser(expr, L, Term, L1),
            L1 = [")" | Rest].
        parser(elem, ["pi" | L], pi(), L):- !.
        parser(elem, ["e" | L], e(), L):- !.
        parser(elem, [S | L], r(R), L):-
            (R = tryToTerm(unsigned, S); R = tryToTerm(real, S)),
            !.
        parser(elem, [S | L], var(S), L):-
            string::isName(S).
    
        parser(items, [S | L], Term1, Term, Rest):-
            addOp(S),
            parser(item, L, Term2, L1),
            !,
            parser(items, L1, bin(S, Term1, Term2), Term, Rest).
        parser(potens, [S | L], Term1, Term, Rest):-
            multOp(S),
            parser(deg, L, Term2, L1),
            !,
            parser(potens, L1, bin(S, Term1, Term2), Term, Rest).
        parser(elems, ["^" | L], Term1, bin("^", Term1, Term2), Rest):-
            parser(deg, L, Term2, Rest),
            !.
        parser(_, L, Term, Term, L).
    
    class facts
        addOp: (string).
        multOp: (string).
        fun: (string).
    clauses
        addOp("+").
        addOp("-").
    
        multOp("*").
        multOp("/").
    
        fun("sin").
        fun("cos").
        fun("exp").
        fun("ln").
    
    class predicates        % вычислитель
        calc: (term) -> real determ.
    clauses
        calc(r(R)) = R.
        calc(pi()) = math::pi.
        calc(e()) = math::e.
        calc(un("-", X)) = - calc(X):- !.
        calc(un(_, X)) = calc(X).
        calc(bin("+", X, Y)) = calc(X) + calc(Y).
        calc(bin("-", X, Y)) = calc(X) - calc(Y).
        calc(bin("*", X, Y)) = calc(X) * calc(Y).
        calc(bin("/", X, Y)) = calc(X) / R:-
            R = calc(Y),
            R <> 0.
        calc(bin("^", X, Y)) = calc(X) ^ calc(Y).
        calc(func("sin", X)) = math::sin(calc(X)).
        calc(func("cos", X)) = math::cos(calc(X)).
        calc(func("exp", X)) = math::exp(calc(X)).
        calc(func("ln", X)) = math::ln(R):-
            R = calc(X),
            R > 0.
    
    class predicates  % преобразование в строку
        toStr: (term) -> string.
    clauses
        toStr(pi()) = "pi".
        toStr(e()) = "e".
        toStr(r(R)) = toString(R).
        toStr(var(V)) = V.
        toStr (un("()", X)) = string::format("(%)", toStr(X)):- !.
        toStr(un(S, X)) = string::format("% %", S, toStr(X)).
        toStr(bin(S, X, Y)) = string::format("% % %", toStr(X), S,
            toStr(Y)).
        toStr(func(S, X)) = string::format("%(%)", S, toStr(X)).
    
        run():-
            S = "-2^3^2 + 3 * (10 - 3) + 20 ^2 + sin(pi / 3)",
            write(S), nl, nl,
            L = scan(S),
            parser(expr, L, Term, Rest),
            write(Rest), nl, nl,
            write(Term), nl, nl,
            write(toStr(Term)),
            R = calc(Term),
            write(" = ", R),
            fail;
            _ = readLine().
    

    Предикат isName проверяет, удовлетворяет ли строка синтаксическим требованиям, предъявляемым к переменным (и ключевым словам). Предикат tryToTerm конвертирует элемент домена string в элемент другого домена. Предикаты exp и ln вычисляют значения экспоненты и натурального логарифма действительного числа (домена real и ureal, соответственно).

    Программа не требует, чтобы строка полностью удовлетворяла грамматике. Выполняется разбор максимального префикса строки, который ей удовлетворяет. Неразобранная часть строки в виде токенов остается в списке Rest.

    В определении предиката calc не используется хвостовая рекурсия. С помощью анонимных предикатов вычисления можно существенно ускорить:

    domains
        operation = (real, real) -> real.
        unOperation = (real) -> real.
    class predicates
        op : (string) -> operation.
        unop : (string) -> unOperation.
        calc: (term) -> real determ.
    clauses
        op("+") = {(X, Y) = X + Y}:- !.
        % …
    
        unop("-") = {(X) = -X}:- !.
        unop("sin") = {(X) = math::sin(X)}:- !.
        % …
    
        calc(bin(S, X, Y)) = op(S)(X, Y).
        % …
    

    Упражнение. Завершите определение вычислителя.

    11.3. Поиск наибольшего общего унификатора

    В данном параграфе реализуется алгоритм поиска наибольшего общего унификатора двух термов (или атомарных формул). Формулы подаются программе в виде двух строк. Парсер преобразует их в термы специального вида, которые и поступают на вход алгоритма.

    Грамматика исходных формул описывается следующим образом:

    term      ::= var  |  const  |  fun [(] termlist [)]
    termlist  ::= term  |  term [,] termlist
    

    Переменные пишутся с прописной буквы, а константы, предикатные и функциональные символы — со строчной, например, $$p(X, f(a, Y, g(b)), h(1, Z))$$.

    Алгоритм поиска наибольшего общего унификатора термов $$T_1$$ и $$T_2$$ с помощью двух стеков $$L$$ и $$S$$ описан в [9]. Он заключается в следующем. Вначале в $$L$$ помещается пара термов $$(T_1, T_2)$$, стек $$S$$ пуст. Алгоритм начинается с шага 1.

  • Если стек $$L$$ пуст, то алгоритм завершает свою работу и унификатор полагается равным $$S$$, иначе совершается переход к шагу 2.
  • Из $$L$$ извлекается пара термов $$(X, Y)$$. Возможны случаи:

  • если $$X$$ — переменная, а $$Y$$ — терм, не содержащий $$X$$, то в $$L$$ и $$S$$ выполняется замена $$X$$ на $$Y$$, из $$S$$ удаляются пары совпадающих термов, в $$S$$ добавляется пара термов $$(X, Y)$$ и выполняется переход к шагу 1;
  • если $$Y$$ — переменная, а $$X$$ — терм, не содержащий $$Y$$, то в $$L$$ и $$S$$ выполняется замена $$Y$$ на $$X$$, из $$S $$удаляются пары совпадающих термов, в $$S$$ добавляется $$(Y, X)$$ и выполняется переход к шагу 1;
  • если $$X$$ и $$Y$$ — пара совпадающих переменных или констант, то выполняется переход к шагу 1;
  • если $$X = f(T_1, \dots, T_k)$$ и $$Y = f(S_1, \dots, S_k)$$, то в $$L$$ добавляются пары термов $$(T_1, S_1), \dots, (T_k, S_k) $$ и выполняется переход к шагу 1;
  • в остальных случаях алгоритм завершает работу — термы не унифицируемы.
  •     open core, console, string
    
    domains
            term = var(string Var); c(string Const); f(string Name, term*).
    
    % парсер
    class predicates
        scan: (string) -> string*.
        parser: (string*, term [out], string* [out]) determ.
        parser: (string*, term* [out]) -> string* determ.
        checkName: (string) determ.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
        parser([S, "(" | L], f(S, TermList), Rest):- !,
            checkName(S),
            [")" | Rest] = parser(L, TermList).
        parser([S | L], var(S), L):-
            isName(S),
            Ch = frontChar(S), 
            Ch = charUpper(Ch),
            !.
        parser([S | L], c(S), L):-
            (_ = tryToTerm(unsigned, S); _ = tryToTerm(real, S);
            checkName(S)),
            !.
    
        parser(["," | L], TermList) = parser(L, TermList):- !.
        parser(L, [Term | TermList]) = parser(Rest, TermList):-
            parser(L, Term, Rest),
            !.
        parser(L, []) = L.
    
        checkName(S):-
            Ch = charToString(frontChar(S)),
            hasAlpha(Ch),
            isLowerCase(Ch).
    
    % поиск наибольшего общего унификатора
    class predicates
        unify: (term, term) -> term*  determ.
        unif: (term*, term*) -> term* determ.
        subterm: (term, term) determ.
        replace: (term*, term, term) -> term*.
        delete: (term*) -> term*.
        put: (term*, term*, term*) -> term* determ.
    clauses
        unify(X, Y) = unif([X, Y], []).
    
        unif([var(X), var(X) | L], Subst) = unif(L, Subst):- !.
        unif([c(X), c(X) | L], Subst) = unif(L, Subst):- !.
        unif([var(X), Y | L], Subst) = unif(replace(L, var(X), Y),
                [var(X), Y | delete(replace(Subst, var(X), Y))]):- !,
            not(subterm(var(X), Y)).
        unif([Y, var(X) | L], Subst) = unif([var(X), Y | L], Subst):- !.
        unif([f(N, TL1), f(N, TL2) | L], Subst) = unif(L1, Subst):-
            L1 = put(TL1, TL2, L).
        unif([], Subst) = Subst.
    
        subterm(X, X):- !.
        subterm(X, f(_, L)):-
            list::exists(L, {(H):- subterm(X, H)}).
    
        replace([X | L], X, Y) = [Y | replace(L, X, Y)]:- !.
        replace([H | L], X, Y) = [H | replace(L, X, Y)].
        replace([], _, _) = [].
    
        delete([X, X | L]) = delete(L):- !.
        delete([X, Y | L]) = [X, Y | delete(L)]:- !.
        delete(_) = [].
    
        put([X | L1], [Y | L2], L) = put(L1, L2, [X, Y | L]).
        put([], [], L) = L.
    
    % преобразование в строку
    class predicates
        s: (term) -> string.
        toStr: (term*) -> string*.
    clauses
        s(var(X)) = X:- !.
        s(c(X)) = X:- !.
        s(f(N, L)) = format("%(%)", N, 
                concatWithDelimiter(list::map(L, {(T) = s(T)}), ", ")).
    
        toStr([X, Y | L]) = [format("% = %", s(X), s(Y)) | toStr(L)]:- !.
        toStr(_) = [].
    
        run():-
            S1 = "p(X, f(b, Y), g(a, a), k(h(1, 2, 3)), U)",
            S2 = "p(a, f(b, a), g(a, Y), k(Z), V)",
            parser(scan(S1), Term1, _),
            parser(scan(S2), Term2, _),
            Subst = unify(Term1, Term2),
            writef("%\n%\n\n%", s(Term1), s(Term2), 
                concatWithDelimiter(toStr(Subst), ", ")),
            fail;
            _ = readLine().
    

    Предикат charToString преобразует символ (char) в строку (string), предикат isLowerCase истинен, если все символы строки имеют нижний регистр, предикат hasAlpha истинен, если строка состоит только из букв, предикат exists проверяет, имеется ли в списке элемент, удовлетворяющий заданному условию.

    Упражнение 2. Реализуйте в программе, приведенной в листинге 11.3, возможность использования анонимных переменных (напомним, что они унифицируются с любыми термами, но не принимают значений).

    Упражнения

  • Напишите программу, которая выполняет перевод простых английских предложений на немецкий язык.
  • Напишите программу, которая разбирает и вычисляет выражения с комплексными числами.
  • Напишите программу, которая разбирает и упрощает тригонометрические выражения.
  • Напишите программу, которая выполняет действия над многочленами — сложение, умножение на число, умножение, деление с остатком.
  • Напишите программу, которая строит польскую запись арифметических выражений.
  • Требуется так расставить между шестью девятками знаки сложения, вычитания умножения и деления, чтобы в результате вычисления получилось число 100.
  • Требуется получить число 24 из трех пятерок и единицы, расставив между ними знаки сложения, вычитания умножения, деления и скобки.
  • Напишите программу, которая выполняет разбор и линеаризует списки вида "[0, [1, [2, 3, [4]]], 5]". Список подается на вход в виде строки. Линейный список также выдается в виде строки.
  • Страницы:

    В настоящей главе изучаются синтаксические анализаторы, или парсеры. Синтаксический анализатор проверяет строку на соответствие грамматике.

    Формальная грамматика — это четверка вида $$\langle N, T, S, P \rangle$$, где $$N$$ — множество нетерминальных символов, $$T$$ — множество терминальных символов, $$S$$ — начальный нетерминальный символ, $$P $$— множество правил. Множества $$N$$ и $$T$$ не пересекаются В контекстно-свободной грамматике правила, в форме Бэкуса-Наура (БНФ), имеют вид:

    $$x ::= X $$

    (используется также обозначение $$x \to X$$), где $$x$$ — нетерминальный символ грамматики, а $$X$$ — последовательность терминальных и нетерминальных символов.

    Язык состоит из фраз — последовательностей терминальных символов. Фраза (или слово, предложение, цепочка символов) принадлежит языку, если она может быть выведена с помощью применения правил грамматики из ее начального символа конечное число раз. Применение правила $$x ::= X$$ преобразует слово $$AxB$$ в слово $$AXB$$.

    Результатом анализа, или разбора строки является дерево разбора, листьями которого являются терминальные символы грамматики, а остальными вершинами — нетерминальные символы.

    11.1. Анализ английских предложений

    Рассмотрим английские предложения вида [4]:

    Every man that lives loves a woman. John likes Mary.

    Грамматику таких предложений можно описать следующим образом:

  • предложение состоит из группы существительного и группы глагола;
  • группа существительного состоит из собственного имени или спецификатора, нарицательного имени и придаточного предложения;
  • придаточное предложение состоит из указательного местоимения и глагольной группы;
  • глагольная группа состоит из непереходного глагола или переходного глагола и группы существительного.
  • Грамматика в БНФ имеет вид:

    sent   ::=  np  vp
    np   ::=  spec1  noun  relcOpt  |  np1
    np1   ::=  pnoun  |  spec2  noun  relcOpt 
    vp   ::=  verb1  |  verb2  np1
    relcOpt ::=  relprn  vp  |  none
    noun   ::=  [man]  |  [woman]  |  [cat]  |  [dog]
    pnoun   ::=  [John]  |  [Mary]
    verb1   ::=  [lives]  |  [sings]  |  [runs]
    verb2   ::=  [loves]  |  [likes]
    spec1   ::=  [every]
    spec2   ::=  [a]
    relprn   ::=  [that]
    

    Множество нетерминальных символов образуют символы из левых частей правил. Терминальные символы заключены в квадратные скобки. Начальным символом грамматики является символ sent.

    Дерево разбора предложения "every man that lives loves a woman" в соответствии с данной грамматикой приведено на рис. 11.1.

    (рис 11.1) Дерево разбора

    В следующей программе строится дерево разбора предложений, удовлетворяющих грамматике. Дерево разбора представляется в виде терма. Домен таких термов определяется рекурсивно (см. программу).

    Первым аргументом предиката parser/4 является нетерминальный символ грамматики. Каждое правило, описывающее этот предикат, соответствует правилу грамматики, указанному в комментарии. На вход предиката также поступает список токенов. Предикат возвращает терм фрагмента дерева разбора и остаток списка токенов.

    Правила грамматики, в правых частях которых отсутствуют нетерминальные символы, реализуются в виде фактов.

        open core, console, string
    
    class predicates      % разбиение на токены
        scan: (string) -> string*.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
    domains      % парсер
        term = sent(term Np, term Vp); np(string, string, term Relc);
            pn(string); vp(string, term Np); relc(string, term Vp); empty.
        nt = sent; np; np1; vp; relc.
    class predicates
        parser: (nt, string*, term [out], string* [out]).
    clauses
    % sent ::= np vp
        parser(sent, L, sent(Np, Vp), Rest):-
            parser(np, L, Np, L1),
            parser(vp, L1, Vp, Rest),
            !.
    
    % np ::= spec1 noun relcOpt
        parser(np, [Spec, Noun | L], np(Spec, Noun, Relc), Rest):-
            spec1(Spec),
            noun(Noun),
            !,
            parser(relc, L, Relc, Rest).
    
    % np ::= np1
        parser(np, L, Np, Rest):- !,
            parser(np1, L, Np, Rest).
    
    % np1 ::= pnoun
        parser(np1, [Name | L], pn(Name1), L):-  
            pnoun(Name1),
            Name = toLowerCase(Name1),
            !.
    
    % np1 ::= spec2 noun relcOpt
        parser(np1, [Spec, Noun | L], np(Spec, Noun, Relc), Rest):-
            spec2(Spec),
            noun(Noun),
            !,
            parser(relc, L, Relc, Rest).
    
    % vp ::= verb1
        parser(vp, [Verb | L], vp(Verb, empty), L):-
            verb1(Verb),
            !.
    
    % vp ::= verb2 np1
        parser(vp, [Verb | L], vp(Verb, Np), Rest):-
            verb2(Verb),
            !,
            parser(np1, L, Np, Rest).
    
    % relcOpt ::= relprn vp
        parser(relc, [Rel | L], relc(Rel, Vp), Rest):-
            relprn(Rel),
            !,
            parser(vp, L, Vp, Rest).
    
    % relcOpt ::= none
        parser(_, L, empty, L).
    
    class facts
        noun: (string).
        pnoun: (string).
        verb1: (string).
        verb2: (string).
        spec1: (string).
        spec2: (string).
        relprn: (string).
    clauses
        noun("man").
        noun("woman").
        noun("cat").
        noun("dog").
    
        pnoun("John").
        pnoun("Mary").
    
        spec1("every").
    
        spec2("a").
    
        verb1("lives").
        verb1("sings").
        verb1("runs").
    
        verb2("loves").
        verb2("likes").
    
        relprn("that").
    
    class predicates      % печать дерева разбора
        print: (term).
        print: (term, charCount).
        f: (charCount, string).
    clauses
        f(N, S):-
            write(string::create(N, "\t"), S), nl.
    
        print(Term):-
            print(Term, 0).
    
        print(sent(Np, Vp), N):-
            f(N, "sent"),
            print(Np, N + 1),
            print(Vp, N + 1).
        print(np(Spec, Noun, Relc), N):-
            f(N, "np"), 
            f(N + 1, "spec"), f(N + 2, Spec),
            f(N + 1, "noun"), f(N + 2, Noun),
            print(Relc, N + 1).
        print(pn(Name), N):-
            f(N, "np"), f(N + 1, Name).
        print(relc(Rel, Vp), N):-
            f(N, "relc"), 
            f(N + 1, "relprn"), f(N + 2, Rel),
            print(Vp, N + 1).
        print(vp(Verb, Np), N):-
            f(N, "vp"), 
            f(N + 1, "verb"), f(N + 2, Verb),
            print(Np, N + 1).
        print(empty, _).
    
        run():-
            Str = "Every man that lives loves a woman.",
            write(Str), nl,
            L = scan(toLowerCase(Str)),
            parser(sent, L, Term, Rest),
            write(Rest), nl,
            write(Term), nl, nl,
            print(Term),
            _ = readLine().
    

    Предикат toLowerCase переводит все символы строки в нижний регистр.

    Упражнение 1. Добавьте в предложения существительные множественного числа. Постройте грамматику и напишите парсер таких предложений.

    11.2. Анализ арифметических выражений

    Рассмотрим арифметические выражения вида:

    -2 + 3 * x; 
    -2^2 * 3 + 2 * 4 / (3 - 1) + (sin(pi/6 + pi/3)) ^ 2 - ln (2 * e^3 - 1); 
    2^3^2 - 3 * (4 + 10^2) * (3 - 10^2); 
    7.
    

    В следующей программе выполняется синтаксический анализ таких выражений. Разбор выполняется в соответствии с праворекурсивной грамматикой, которая имеет следующий вид:

    expr  ::=  addSignOpt  item  items
    item  ::=  deg  potens
    deg  ::=  elem  elems
    elem  ::=  fun  [(]  expr  [)]  |  [(]  expr  [)]  |  [pi]  |  [e]  |  num  |  var
    items  ::=  addSign  item  items  |  none
    potens   ::=  multSign  deg  potens |  none
    elems  ::=  [^]  elem  elems  |  none
    addSignOpt  ::=  addSign  |  none
    addSign   ::=  [+]  |  [-]
    multSign  ::=  [*]  |  [\]
    fun  ::=  [cos]  |  [sin]  |  [exp]  |  [ln]
    

    Сначала программа строит терм дерева разбора. Если выражение не содержит переменных, то по терму вычисляется его значение. Затем выполняется обратное преобразование терма в строку. Предикат parser/4 по входному списку токенов и нетерминальному символу грамматики (expr, item, deg или elem) возвращает подтерм дерева разбора и остаток списка токенов. Предикат parser/5 имеет еще один аргумент — терм, который соответствует первому аргументу бинарной операции (сложения, вычитания, умножения, деления или возведения в степень). Он рекурсивно строит терм для последовательности аргументов этой операции (см. правила для символов items, potens и elems).

    class predicates
        scan: (string) -> string*.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            string::frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
    domains                                                 % парсер
        term = un(string Op, term); bin(string Op, term, term);
            func(string Op, term); var(string); r(real); pi(); e().
        nt = expr; item; items; deg; potens; elem; elems.
    class predicates
        parser: (nt, string*, term [out], string* [out]) determ.
        parser: (nt, string*, term, term [out], string* [out]).
    clauses
        parser(expr, [S | L], Term, Rest):-
            addOp(S),
            !,
            parser(item, L, Term1, L1),
            parser(items, L1, un(S, Term1), Term, Rest).
        parser(expr, L, Term, Rest):-
            parser(item, L, Term1, L1),
            parser(items, L1, Term1, Term, Rest).
        parser(item, L, Term, Rest):-
            parser(deg, L, Term1, L1),
            parser(potens, L1, Term1, Term, Rest).
        parser(deg, L, Term, Rest):-
            parser(elem, L, Term1, L1),
            parser(elems, L1, Term1, Term, Rest).
        parser(elem, [S, "(" | L], func(S, Term), Rest):-
            fun(S),
            !,
            parser(expr, L, Term, L1),
            L1 = [")" | Rest].
        parser(elem, ["(" | L], un("()", Term), Rest):- !,
            parser(expr, L, Term, L1),
            L1 = [")" | Rest].
        parser(elem, ["pi" | L], pi(), L):- !.
        parser(elem, ["e" | L], e(), L):- !.
        parser(elem, [S | L], r(R), L):-
            (R = tryToTerm(unsigned, S); R = tryToTerm(real, S)),
            !.
        parser(elem, [S | L], var(S), L):-
            string::isName(S).
    
        parser(items, [S | L], Term1, Term, Rest):-
            addOp(S),
            parser(item, L, Term2, L1),
            !,
            parser(items, L1, bin(S, Term1, Term2), Term, Rest).
        parser(potens, [S | L], Term1, Term, Rest):-
            multOp(S),
            parser(deg, L, Term2, L1),
            !,
            parser(potens, L1, bin(S, Term1, Term2), Term, Rest).
        parser(elems, ["^" | L], Term1, bin("^", Term1, Term2), Rest):-
            parser(deg, L, Term2, Rest),
            !.
        parser(_, L, Term, Term, L).
    
    class facts
        addOp: (string).
        multOp: (string).
        fun: (string).
    clauses
        addOp("+").
        addOp("-").
    
        multOp("*").
        multOp("/").
    
        fun("sin").
        fun("cos").
        fun("exp").
        fun("ln").
    
    class predicates        % вычислитель
        calc: (term) -> real determ.
    clauses
        calc(r(R)) = R.
        calc(pi()) = math::pi.
        calc(e()) = math::e.
        calc(un("-", X)) = - calc(X):- !.
        calc(un(_, X)) = calc(X).
        calc(bin("+", X, Y)) = calc(X) + calc(Y).
        calc(bin("-", X, Y)) = calc(X) - calc(Y).
        calc(bin("*", X, Y)) = calc(X) * calc(Y).
        calc(bin("/", X, Y)) = calc(X) / R:-
            R = calc(Y),
            R <> 0.
        calc(bin("^", X, Y)) = calc(X) ^ calc(Y).
        calc(func("sin", X)) = math::sin(calc(X)).
        calc(func("cos", X)) = math::cos(calc(X)).
        calc(func("exp", X)) = math::exp(calc(X)).
        calc(func("ln", X)) = math::ln(R):-
            R = calc(X),
            R > 0.
    
    class predicates  % преобразование в строку
        toStr: (term) -> string.
    clauses
        toStr(pi()) = "pi".
        toStr(e()) = "e".
        toStr(r(R)) = toString(R).
        toStr(var(V)) = V.
        toStr (un("()", X)) = string::format("(%)", toStr(X)):- !.
        toStr(un(S, X)) = string::format("% %", S, toStr(X)).
        toStr(bin(S, X, Y)) = string::format("% % %", toStr(X), S,
            toStr(Y)).
        toStr(func(S, X)) = string::format("%(%)", S, toStr(X)).
    
        run():-
            S = "-2^3^2 + 3 * (10 - 3) + 20 ^2 + sin(pi / 3)",
            write(S), nl, nl,
            L = scan(S),
            parser(expr, L, Term, Rest),
            write(Rest), nl, nl,
            write(Term), nl, nl,
            write(toStr(Term)),
            R = calc(Term),
            write(" = ", R),
            fail;
            _ = readLine().
    

    Предикат isName проверяет, удовлетворяет ли строка синтаксическим требованиям, предъявляемым к переменным (и ключевым словам). Предикат tryToTerm конвертирует элемент домена string в элемент другого домена. Предикаты exp и ln вычисляют значения экспоненты и натурального логарифма действительного числа (домена real и ureal, соответственно).

    Программа не требует, чтобы строка полностью удовлетворяла грамматике. Выполняется разбор максимального префикса строки, который ей удовлетворяет. Неразобранная часть строки в виде токенов остается в списке Rest.

    В определении предиката calc не используется хвостовая рекурсия. С помощью анонимных предикатов вычисления можно существенно ускорить:

    domains
        operation = (real, real) -> real.
        unOperation = (real) -> real.
    class predicates
        op : (string) -> operation.
        unop : (string) -> unOperation.
        calc: (term) -> real determ.
    clauses
        op("+") = {(X, Y) = X + Y}:- !.
        % …
    
        unop("-") = {(X) = -X}:- !.
        unop("sin") = {(X) = math::sin(X)}:- !.
        % …
    
        calc(bin(S, X, Y)) = op(S)(X, Y).
        % …
    

    Упражнение. Завершите определение вычислителя.

    11.3. Поиск наибольшего общего унификатора

    В данном параграфе реализуется алгоритм поиска наибольшего общего унификатора двух термов (или атомарных формул). Формулы подаются программе в виде двух строк. Парсер преобразует их в термы специального вида, которые и поступают на вход алгоритма.

    Грамматика исходных формул описывается следующим образом:

    term      ::= var  |  const  |  fun [(] termlist [)]
    termlist  ::= term  |  term [,] termlist
    

    Переменные пишутся с прописной буквы, а константы, предикатные и функциональные символы — со строчной, например, $$p(X, f(a, Y, g(b)), h(1, Z))$$.

    Алгоритм поиска наибольшего общего унификатора термов $$T_1$$ и $$T_2$$ с помощью двух стеков $$L$$ и $$S$$ описан в [9]. Он заключается в следующем. Вначале в $$L$$ помещается пара термов $$(T_1, T_2)$$, стек $$S$$ пуст. Алгоритм начинается с шага 1.

  • Если стек $$L$$ пуст, то алгоритм завершает свою работу и унификатор полагается равным $$S$$, иначе совершается переход к шагу 2.
  • Из $$L$$ извлекается пара термов $$(X, Y)$$. Возможны случаи:

  • если $$X$$ — переменная, а $$Y$$ — терм, не содержащий $$X$$, то в $$L$$ и $$S$$ выполняется замена $$X$$ на $$Y$$, из $$S$$ удаляются пары совпадающих термов, в $$S$$ добавляется пара термов $$(X, Y)$$ и выполняется переход к шагу 1;
  • если $$Y$$ — переменная, а $$X$$ — терм, не содержащий $$Y$$, то в $$L$$ и $$S$$ выполняется замена $$Y$$ на $$X$$, из $$S $$удаляются пары совпадающих термов, в $$S$$ добавляется $$(Y, X)$$ и выполняется переход к шагу 1;
  • если $$X$$ и $$Y$$ — пара совпадающих переменных или констант, то выполняется переход к шагу 1;
  • если $$X = f(T_1, \dots, T_k)$$ и $$Y = f(S_1, \dots, S_k)$$, то в $$L$$ добавляются пары термов $$(T_1, S_1), \dots, (T_k, S_k) $$ и выполняется переход к шагу 1;
  • в остальных случаях алгоритм завершает работу — термы не унифицируемы.
  •     open core, console, string
    
    domains
            term = var(string Var); c(string Const); f(string Name, term*).
    
    % парсер
    class predicates
        scan: (string) -> string*.
        parser: (string*, term [out], string* [out]) determ.
        parser: (string*, term* [out]) -> string* determ.
        checkName: (string) determ.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
        parser([S, "(" | L], f(S, TermList), Rest):- !,
            checkName(S),
            [")" | Rest] = parser(L, TermList).
        parser([S | L], var(S), L):-
            isName(S),
            Ch = frontChar(S), 
            Ch = charUpper(Ch),
            !.
        parser([S | L], c(S), L):-
            (_ = tryToTerm(unsigned, S); _ = tryToTerm(real, S);
            checkName(S)),
            !.
    
        parser(["," | L], TermList) = parser(L, TermList):- !.
        parser(L, [Term | TermList]) = parser(Rest, TermList):-
            parser(L, Term, Rest),
            !.
        parser(L, []) = L.
    
        checkName(S):-
            Ch = charToString(frontChar(S)),
            hasAlpha(Ch),
            isLowerCase(Ch).
    
    % поиск наибольшего общего унификатора
    class predicates
        unify: (term, term) -> term*  determ.
        unif: (term*, term*) -> term* determ.
        subterm: (term, term) determ.
        replace: (term*, term, term) -> term*.
        delete: (term*) -> term*.
        put: (term*, term*, term*) -> term* determ.
    clauses
        unify(X, Y) = unif([X, Y], []).
    
        unif([var(X), var(X) | L], Subst) = unif(L, Subst):- !.
        unif([c(X), c(X) | L], Subst) = unif(L, Subst):- !.
        unif([var(X), Y | L], Subst) = unif(replace(L, var(X), Y),
                [var(X), Y | delete(replace(Subst, var(X), Y))]):- !,
            not(subterm(var(X), Y)).
        unif([Y, var(X) | L], Subst) = unif([var(X), Y | L], Subst):- !.
        unif([f(N, TL1), f(N, TL2) | L], Subst) = unif(L1, Subst):-
            L1 = put(TL1, TL2, L).
        unif([], Subst) = Subst.
    
        subterm(X, X):- !.
        subterm(X, f(_, L)):-
            list::exists(L, {(H):- subterm(X, H)}).
    
        replace([X | L], X, Y) = [Y | replace(L, X, Y)]:- !.
        replace([H | L], X, Y) = [H | replace(L, X, Y)].
        replace([], _, _) = [].
    
        delete([X, X | L]) = delete(L):- !.
        delete([X, Y | L]) = [X, Y | delete(L)]:- !.
        delete(_) = [].
    
        put([X | L1], [Y | L2], L) = put(L1, L2, [X, Y | L]).
        put([], [], L) = L.
    
    % преобразование в строку
    class predicates
        s: (term) -> string.
        toStr: (term*) -> string*.
    clauses
        s(var(X)) = X:- !.
        s(c(X)) = X:- !.
        s(f(N, L)) = format("%(%)", N, 
                concatWithDelimiter(list::map(L, {(T) = s(T)}), ", ")).
    
        toStr([X, Y | L]) = [format("% = %", s(X), s(Y)) | toStr(L)]:- !.
        toStr(_) = [].
    
        run():-
            S1 = "p(X, f(b, Y), g(a, a), k(h(1, 2, 3)), U)",
            S2 = "p(a, f(b, a), g(a, Y), k(Z), V)",
            parser(scan(S1), Term1, _),
            parser(scan(S2), Term2, _),
            Subst = unify(Term1, Term2),
            writef("%\n%\n\n%", s(Term1), s(Term2), 
                concatWithDelimiter(toStr(Subst), ", ")),
            fail;
            _ = readLine().
    

    Предикат charToString преобразует символ (char) в строку (string), предикат isLowerCase истинен, если все символы строки имеют нижний регистр, предикат hasAlpha истинен, если строка состоит только из букв, предикат exists проверяет, имеется ли в списке элемент, удовлетворяющий заданному условию.

    Упражнение 2. Реализуйте в программе, приведенной в листинге 11.3, возможность использования анонимных переменных (напомним, что они унифицируются с любыми термами, но не принимают значений).

    Упражнения

  • Напишите программу, которая выполняет перевод простых английских предложений на немецкий язык.
  • Напишите программу, которая разбирает и вычисляет выражения с комплексными числами.
  • Напишите программу, которая разбирает и упрощает тригонометрические выражения.
  • Напишите программу, которая выполняет действия над многочленами — сложение, умножение на число, умножение, деление с остатком.
  • Напишите программу, которая строит польскую запись арифметических выражений.
  • Требуется так расставить между шестью девятками знаки сложения, вычитания умножения и деления, чтобы в результате вычисления получилось число 100.
  • Требуется получить число 24 из трех пятерок и единицы, расставив между ними знаки сложения, вычитания умножения, деления и скобки.
  • Напишите программу, которая выполняет разбор и линеаризует списки вида "[0, [1, [2, 3, [4]]], 5]". Список подается на вход в виде строки. Линейный список также выдается в виде строки.
  • Вернуться к учебному плану