Порождающие грамматики Хомского

в 18:05, , рубрики: Алгоритмы, математика, Программирование

Небольшое предисловие

Этот текст является продолжением поста , в котором автор попытался как можно более просто и без сложных математических выкладок описать понятия формального языка и грамматики. На этот текст пришло достаточно много откликов и автор счел себя обязанным написать продолжение.

Ниже описывается формализм порождающих грамматик Хомского. Методы задания языка с помощью порождающих грамматик сейчас довольно популярны, особенно для машинной обработки компьютерных языков. Но обычно изучение порождающих грамматик в теории трансляторов заканчивается на контекстно-свободных грамматиках. Последние являются довольно узким специальным классом порождающих грамматик Хомского и обычно используются как вид категориальных грамматик (как конкретно это делается, будет показано ниже) для задания синтаксических анализаторов. Последнее обстоятельство только затуманивает понимание подхода Хомского. Дальнейшее изложение предназначено тем, кому интересно понять, в чем состоит этот подход.

Определение порождающей грамматики

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

Формализм порождающих грамматик Хомского был введен Ноамом Хомским в конце 50-х годов прошлого века. За короткое время этот формализм обрел необычайную популярность. Некоторое время порождающие грамматики рассматривались как панацея — универсальный подход для задания всевозможных языков, в том числе и естественных (т.е. языков, которые люди используют для повседневного общения между собой). Но время показало, что порождающие грамматики для описания естественных языков не очень удобны. Сейчас порождающие грамматики применяются, в основном, для описания синтаксиса формальных языков, подобных языкам программирования и другим компьютерным языкам.

Порождающая грамматика Хомского задается как множество «правил порождения» (продукций). Каждое правило является просто парой цепочек (w', w'') и задает возможность замены левой цепочки на правую при генерации цепочек языка, задаваемого грамматикой. По этой причине, правила обычно записывают в виде w' --> w'', указывая конкретно, что на что можно заменять. Множество правил в грамматике должно быть непустым и конечным, и обычно обозначается латинской P.

Цепочки в правилах грамматики могут быть составлены из символов двух алфавитов: алфавита терминальных символов (терминалов) и алфавита нетерминальных символов (нетерминалов). Алфавит терминалов обозначают через T. Этот алфавит на самом деле совпадает с алфавитом того формального языка, который задает данная грамматика. Смысл термина «терминальный» состоит в том, что в правилах грамматики в левой части не может быть цепочек, которые составлены только из терминальных символов. Поэтому, если такая цепочка получилась в результате подстановки, то следующая процесс порождения цепочки остановится (terminate). Нетерминальные символы используются в промежуточных порождениях цепочек. Смысл нетерминала в задании алгоритма порождения цепочки может быть самый разный и обычно зависит от типа грамматики, в которой этот символ используется. Различные примеры использования нетерминальных символов будут рассмотрены ниже.

Но один нетерминальный символ всегда имеет один и тот же смысл — он обозначает все цепочки языка. Называется этот нетерминал «начальным нетерминальным символов порождающей грамматики» и обычно обозначается посредством латинского S (start или sentence). В каждой порождающей грамматике обязательно должно быть правило, к которого левая часть состоит из единственного начального нетерминала, иначе в данной грамматике нельзя будет породить даже одной цепочки.

Итак, порождающая грамматика Хомского — это четверка G = {N, T, P, S}, где

  • N — конечный алфавит нетерминальных символов.
  • T — конечный алфавит терминальных символов (совпадает с алфавитом языка, задаваемого грамматикой).
  • P — конечное множество правил порождения.
  • S — начальный нетерминал грамматики G.

Язык порождающей грамматики

Порождающая грамматика Хомского задает язык посредством конечного числа подстановок цепочек из начального нетерминала грамматики на основе правил порождения. Опишем это чуть более конкретно.

Шаг порождения w' alpha w'' => w' beta w'' состоит в замене подцепочки alpha на подцепочку beta в соответствии с правилом порождения alpha --> beta. При этом, очевидно, из цепочки w' alpha w'' получается цепочка w' beta w''. Иначе говоря, если имеется некоторая цепочка и некоторая ее подцепочка является левой частью какого-то правила грамматики, то мы имеем полное право заменить эту левую часть правила на правую. Конечная последовательность шагов порождений называется порождением. Нуль или более порождений будет обозначать знаком =>*. Обозначение alpha =>* beta говорит о том, что цепочка beta получена из цепочки alpha конечным числом подстановок на основе правил порождения. В этом обозначении может быть так, что подстановка (порождение) не была применена ни разу, в этом случае цепочка alpha совпадает с beta.

Итак, язык порождающей грамматики G = {N, T, P, S} — это множество цепочек, составленных из терминальных символов и порожденных из начального символа грамматики. Математическая формула такова: L = {w | S =>* w}.

Для иллюстрации приведем два простых примера.

Пример очень простого языка

Пусть язык L состоит из одной цепочки, которая состоит из единственного символа a. Иначе говоря, L = {a}. Для порождения цепочки a достаточно одного правила S --> a. Единственное порождение, которое может быть в данной грамматике — это S => a.

Следует заметить, что для этого языка можно было бы ввести еще один нетерминальный символ, скажем, символ A, а также правила S --> A и A --> a. Тогда единственным порождением было бы следующее: S => A => a. Так как алфавит нетерминалов грамматики мы выбираем произвольно, становится понятно, что даже для такого простого языка имеется бесконечное множество порождающих грамматик, задающих данный язык.

Язык простых арифметических выражений

Рассмотрим язык A = {a+a, a+a+a, a+a+a+a, ...}. Цепочки этого языка представляют собой последовательности символов a, разделенных символами +. Как задать правила порождения этого языка? Заметим, что каждая цепочка языка начинается с символа a за которым идет одна или более цепочек +a. Соответственно, возникает мысль сначала породить символ a, а затем каждая цепочка языка будет получаться присоединением к этому символу справа одной или более цепочек +a. Чтобы отделить эти две стадии порождения друг от друга, введем нетерминальный символ A. Тогда, получим грамматику со следующими правилами: S --> aA, A --> +aA, A --> +a.

Рассмотрим, например, как можно породить цепочку a+a+a. S => aA => a+aA => a+a+a. В этом порождении последовательно были применены все три правила: S --> aA, A --> +aA, A --> +a.

Язык A содержит бесконечное число цепочек, значит, ограничения на длину цепочки в этом языке нет. Единственный способ порождать цепочки неограниченной длины, это использовать рекурсивные правила порождения, т.е. правила, в которых в правой части правила содержится его левая часть. В примере выше это правило A --> +aA. Левая часть — это цепочка из единственного символа A, который также содержится и в правой части. Такая рекурсия позволяет последовательно применять в подстановке одно и то же правила, увеличивая, сколько необходимо, длину порождаемой цепочки. Рекурсия может быть и опосредованной, через промежуточные правила. Например, правила A --> aBc, B --> deA задают опосредованную рекурсию цепочки A.

Классы грамматик

Ноам Хомский ввел классы грамматик (и соответствующие классы языков) задавая ограничения на вид правил порождающей грамматики. Каждый класс грамматик имеет свою описательную мощность. Описательную мощность класса грамматик можно охарактеризовать, как возможность выражений в правилах грамматики определенных синтаксических отношений. Рассмотрим, как классы грамматик задают синтаксические отношения.

Грамматики типа 3

Этот класс грамматик задает алгоритм порождения цепочек присоединением некоторого количества терминальных символов с правого или левого края порождаемой цепочки. Очевидно, что правила для такого метода порождения должны иметь вид A --> alpha B или A --> B alpha, где alpha — цепочка, состоящая из терминальных символов. В этом случае, если имеется промежуточная (в процессе порождения) цепочка X1..Xn A, то замена в соответствии с правилом A --> alpha B даст цепочку X1..Xn alpha B. Например, для правил S --> aaaA, A --> abcA и A --> bbb можно задать порождение S => aaaA => aaaabcA => aaaabcbbb.

Синтаксическое отношение, которое задается грамматиками типа 3, можно обозначить термином «быть рядом». Под «рядом» здесь подразумевается как непосредственно рядом, если это задано в правой части какого-то правила порождения, так и опосредованно рядом, через нетерминальные символы в связанных между собой правилах порождения.

Для математической строгости строку терминальных символов в правилах грамматик типа 3 разбивают на несколько правил с одним терминальным символом в правой части. Например, если имеется правило A --> abcB, то его можно заменить на следующие правила, применение которых в результате порождает ту же цепочку: A --> a A1, A1 --> b A2, A2 --> cB. Иначе говоря, подстановка A => abcB эквивалентна последовательности подстановок A => a A1 => a b A2 => abcB. Такие грамматики, где нетерминальный символ стоит справа в правой части правила, называют праволинейными грамматиками, если в правой части нетерминальный символ стоит слева от терминала, то грамматику называют леволинейной.

Зададим, например леволинейную грамматику для языка A = {a+a, a+a+a, a+a+a+a, ...}. Правила грамматики типа 3, как было рассмотрено выше, это: S --> aA, A --> +aA, A --> +a. Здесь цепочки порождаются присоединением пары символов справа. Изменим грамматику так, чтобы символы присоединялись слева, а также добавим нетерминальные символы, чтобы каждый раз добавлять только по одному символу. Получим грамматику:
S --> Aa
A --> B+
B --> Aa
B --> a
Вот как выглядит порождение цепочки a+a+a: S => Aa => B+a => Aa+a => B+a+a => a+a+a.

Внимательный читатель вероятно заметил, что грамматика типа 3 похожа на попрождающий автомат, в котором роль состояний играют нетерминальный символы грамматики. Одна из возможных интерпретаций этой грамматики — это, действительно, конечный автомат.

Контекстно-свободные грамматики

Контекстно-свободные грамматики имеют правила вида: A --> alpha. В левой части правила должен стоять один символ (конечно, нетерминальный), а справа может быть любая цепочка из терминальных и нетерминальных символов (в том числе и пустая).

КС-грамматики задают два вида синтаксических отношений: отношение «быть рядом» и отношение «быть частью» или отношение иерархии. Отношение иерархии наиболее естественно для человеческого ума. Человеку свойственно типизировать вещи, т.е. рассматривать конкретные объекты своего мышления как части какого-то общего типа (класса). Каждая вещь, о которой думает человек, является экземпляром некоторого класса. Например, конкретный стул является экземпляром класса «стул» с соответствующими признаками. Человеческому уму также свойственно разделять типы на подтипы, двигаясь от более конкретных типов к более абстрактным. Скажем, стул есть подтип типа мебель, мебель есть подтип типа предмет, предмет есть подтип типа объект и т.п. Отношение «тип-подтип» и есть отношение иерархии.

КС-грамматика может быть проинтерпретирована как категориальная грамматика, т.е. грамматика типов. Символы грамматики в этом случае могут мыслиться как типы, а правила тогда задают отношение иерархии между типами. Нетерминальные символы выступают как сложные типы, а терминальный символы — как атомарные типы, у которых не может быть подтипов. Такая интерпретация КС-грамматики очень популярна и часто используется при создании трансляторов языков. Но, задавая класс КС-грамматик, Хомский имел ввиду нечто другое.

Ввиду того, что КС-грамматика является порождающей, она задает алгоритм (строго говоря, не алгоритм, но исчисление — многовариантный алгоритм) порождения цепочек языка. Порождение здесь задается не только присоединением цепочек справа или слева имеющейся цепочки, но и вставкой цепочки куда-нибудь внутрь имеющейся. Вставка производится заменой нетерминального символа в цепочке на цепочку, которая стоит в правой части некоторого правила, в левой части которого находится этот нетерминал. Скажем, цепочку aabBBACbbb можно преобразовать в цепочку aabBBaaaCbbb, если есть правило A --> aaa. В этом смысле, порождаемая цепочка растет не равномерно с какого-то края, но как-бы «пухнет» изнутри.

Проиллюстрируем сказанное на примере. Рассмотрим язык L = {a^n b^n | n = 1, 2, 3,...}. Выражение a^n здесь означает повторение n раз символа a. Таким образом, язык L состоит из цепочке вида ab, aabb, aaabbb и т.д. Зададим КС-грамматику для этого языка. Для этого заметим, что из цепочки языка можно получить другую цепочку языка, присоединяя к первой слева символ a, а справа символ b. Скажем, если имеется цепочка aabb, то из нее можно получить цепочку aaabbb. Это замечание дает правило порождения S --> aSb (напомним, что цепочки языка порождаются из начального нетерминала грамматики и, значит, могут быть обозначены этим символом). Есть еще специальный случай цепочки, которая не дробится на более мелкие — это цепочка ab. Введем для ее порождения правило S --> ab. Итак, грамматика языка имеет правила: S --> aSb и S --> ab. Зададим порождение цепочки aaabbb: S => aSb => aaSbb => aaabbb.

Контекстно-зависимые грамматики и грамматики без ограничений

В правилах КС-грамматики нетерминальный символ в левой части правила порождения можно менять на правую часть в любом месте порождаемой цепочки, где бы этот символ не встретился. Но иногда хотелось бы различать контексты, в которых находится символ в цепочке, и в одних случаях производить замену символа, в других — нет. Правила КС-грамматики этого делать не позволяют, поэтому для таких случаев необходимы правила специального вида.

Контекстно-зависимая грамматика имеет правила вида w' A w'' --> w' alpha w''. Здесь w' и w'' — цепочки (может быть пустые), составленные из терминальных и нетерминальных символов грамматики, alpha — непустая цепочка из тех же символов. Иначе говоря, нетерминальный символ A заменяется на цепочку alpha в контексте цепочек w' и w''.

С КЗ-грамматикой связан другой класс грамматик — неукоращивающие грамматики. Правила в таких грамматиках должны удовлетворять одному условию: длина правой части должна быть не меньше длины левой части. Так как в правилах КЗ-грамматик имеется условие, чтобы цепочка alpha была непустая, то эти грамматики также являются неукорачивающими. Но, самое интересное состоит в том, что для каждого языка, заданного неукорачивающей грамматикой, может быть придумана КЗ-грамматика, задающая тот же язык. Иначе говоря, классы языков, задаваемых КЗ-грамматиками и неукорачивающими грамматиками, совпадают.

Зачем так необходимо выделять класс языков, задаваемых неукорачивающими грамматиками? Дело в том, что для таких языков можно задать распознающий автомат. Распознающая грамматика конструируется следующим образом: получая на вход цепочку, последовательно делаем порождения, упорядочивая их по длине порождаемой цепочки. Т.к. грамматика неукорачивающая, то таких порождений будет конечное множество и, если среди них не нашлось совпадения с данной на вход цепочкой, то напечатать «нет».

Для грамматики без ограничений на вид правил такой алгоритм распознавания в общем случае построить нельзя. Порождаемая цепочка может вести себя как «гармошка», раздуваясь и сдуваясь в процессе порождения. Поэтому достижение порождаемой цепочкой определенной длины не гарантирует, что далее в процессе порождения не будет получена поданная на вход цепочка.

Действительно ли КЗ-языки образуют собственный класс, не совпадает ли этот класс с классом КС-языков. Иначе говоря, есть ли язык, для которого гарантировано нельзя задать КС-грамматику, но можно задать КЗ-грамматику? Ответ: да, такие языки есть. В качестве примера такого языка можно привести следующий язык L = {ww}. Цепочки этого языка составлены из двух повторяющихся цепочек над каким-то алфавитом. Доказывать, что для этого языка нельзя построить КС-грамматики мы здесь не будем. КЗ-грамматику можно задать на основе следующего соображения. Сначала сгенерировать цепочку w и нетерминальный символ, скажем A, т.е. получить цепочку Aw. Затем продвинуть символ A сквозь цепочку w, генерируя по ходу копии символов этой цепочки, после чего продвинуть эти символы направо. Примерно то же, как это будет реализовано в примере ниже.

Рассмотрим пример задания грамматики для языка L = {a^n^2 | n = 1, 2, 3, ...}. Цепочки этого языка состоят из символов a, причем число этих символов есть квадрат натурального числа: 1, 4, 9, 25 и т.д. Мы зададим для этого языка грамматику без ограничений. Генерация цепочек будет состоять из следующих этапов:

  1. Генерация n символов для некоторого натурального числа n.
  2. Порождение из этого числа символов n^2 символов.
  3. Преобразование этих символов в символы a.

Для реализации первого этапа добавляем правила
S --> LS'R
S' --> AS'B
S' --> AB

Первым правилом оборачиваем порождаемую цепочку в ограничители L и R. Они нам понадобятся в дальнейшем для реализации третьей фазы генерации. Оставшиеся два правила просто генерируют цепочку вида AA...ABB...B, где число символов A и B совпадает.

Теперь необходимо породить n^2 символов на основе цепочке AA...ABB...B. Это делает простым приемом. Двигаем символы B налево и, при каждом переходе через символ A, порождать еще один символ C. Через символы C символ A может свободно проходить направо, а символ B — налево. Правила для этого этапа следующие:
AB --> BAC
AC --> CA
CB --> BC
Когда все символы B дойдут до левого края, перейдя символы A, символов C будет ровно n^2.

Теперь надо освободиться от символов L, A, B и R, а также преобразовать символы C в символы a. Для этого аннигилируем символ B при его проходе до левого края, т.е. до символа L. Соответственно поступаем и с символом A на правом крае. При реализации такой стратегии останется цепочка вида LCC....CR. Чтобы избавится от символов L и R, начинаем двигать левый ограничитель к правому и, при их соприкосновении, уничтожаем эти символы. Заодно, при прохождении через символы L, преобразуем их в символы a. Приведем правила для этой фазы генерации.
LB --> L
AR --> R
LC --> aL
LR --> epsilon
Здесь epsilon обозначает пустую цепочку.

Приведем в качестве примера порождение цепочки aaaa: S => LS'R => LAS'BR => LAABBR => LABACBR => LBACACBR => LACACBR => LACABCR => LACBACCR => LABCACCR => LBACCACCR => LACCACCR => LCACACCR => LCCAACCR => LCCACACR => LCCACCAR => LCCACCR => LCCCACR => LCCCCAR => LCCCCR => aLCCCR => aaLCCR => aaaLCR => aaaaLR => aaaa

Заключение

Автор надеется, что последний пример ясно продемонстрировал читателю, что порождающая грамматика Хомского представляет собой своего рода программу, предназначенную для генерации цепочек формального языка, задаваемого этой грамматикой. Язык задания программы довольно специфический, соответственно и реализация «программ генерации» (грамматик) требует опыта и определенной привычки в их написании.

Порождающие грамматики Хомского основаны на глубоких идеях и нет ничего удивительного в том, что какие-то подклассы этого вида грамматик генерируют не только определенный вида языков, но и пересекаются с идеями из других разделов математической лингвистики. К таких разделам относятся категориальные грамматики и распознающие автоматы. Конечно, в этом тексте описаны только основные идеи, теория порождающих грамматик шире и глубже, чтобы ее можно было описать в рамках одной статьи.

Автор: mefrill

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js