Грамматика в программировании – это набор правил для разбора текста. Это очень полезная вещь – к примеру, грамматику можно использовать для проверки того, подчиняется ли строка текста конкретным стандартам или нет. У Perl 6 есть встроенная поддержка грамматик. Их настолько просто создавать, что единожды начав, вы обнаружите, что используете их везде.
В последнее время я работал над Module::Minter, простым приложением для создания базовой структуры модуля Perl 6. Мне надо было проверить, что предлагаемое имя модуля соответствует стандартам именования Perl 6.
Имена модулей – это идентификаторы, разделённые двумя двоеточиями. Идентификатор должен начинаться с алфавитного символа (a-z) или подчёркивания, за которым могут идти алфавитно-цифровые символы. Правда, у некоторых модулей может быть только один идентификатор, без двоеточий, а у других их может быть много (HTTP::Server::Async::Plugins::Router::Simple).
Определяем грамматику
В Perl 6 грамматики строятся на регулярках. Мне нужны две: одна для идентификаторов, другая – для разделителей в виде двоеточий. Для идентификаторов я задал:
<[A..Za..z_]> # начинается с буквы или подчёркивания <[A..Za..z0..9]> ** 0..* # ноль или больше алфавитно-цифровых
Помните, что мы используем регулярки из Perl 6, и тут всё выглядит несколько по-другому. Класс символа определяется <[… ]>, а диапазон определяется оператором… вместо тире. Эта регулярка совпадает с любой первой буквой или подчёркиванием, за которым идёт ноль или более алфавитно-цифровых символов.
С двумя двоеточиями всё проще:
\:\: # пары двоеточий
Грамматики определяют при помощи ключевого слова grammar, за которым идёт название. Назову-ка я эту грамматику Legal::Module::Name
grammar Legal::Module::Name { ... }
Теперь можно добавлять в неё токены-регулярки:
grammar Legal::Module::Name { token identifier { # первый символ - буква или _ <[A..Za..z_]> <[A..Za..z0..9]> ** 0..* } token separator { \:\: # пары двоеточий } }
Каждой грамматике нужно задать токен TOP, который обозначает её начало.
grammar Legal::Module::Name { token TOP { # идентификатор, за которым идёт ноль или более пар separator - identifier ^ [] ** 0..* $ } token identifier { # первый символ - буква или _ <[A..Za..z_]> <[A..Za..z0..9]> ** 0..* } token separator { \:\: # пары двоеточий } }
Токен TOP определяет, что разрешённое имя модуля начинается с токена identifier, за которым идут ноль или больше пар токенов separator и identifier. Поддерживать такую штуку очень просто – если б я захотел изменить правила так, чтобы разделители содержали тире, я бы обновил регулярку только в одном токене.
Использование грамматики
Метод parse прогоняет грамматику на строке, и в случае успеха возвращает объект match. Следующий код обрабатывает строку $proposed_module_name, и либо выводит объект match, либо сообщение об ошибке.
my $proposed_module_name = 'Super::New::Module'; my $match_obj = Legal::Module::Name.parse($proposed_module_name); if $match_obj { say $match_obj; } else { say 'Да что ж это за имя модуля-то такое, а?!'; } Вывод: 「Super::New::Module」 identifier => 「Super」 separator => 「::」 identifier => 「New」 separator => 「::」 identifier => 「Module」
Извлекаем содержимое объекта match
Можно не вываливать всё содержимое объекта match, а извлечь сыгравшие токены. В следующем коде используются именованные регулярки и ключи хэшей.
say $match_obj[0].Str; # Super say $match_obj[1].Str; # New say $match_obj[2].Str; # Module say $match_obj; # все три
Action Classes (классы действий)
Perl 6 даёт возможность добавить класс действий, определяющий дополнительное поведение для сыгравших токенов. Допустим, я хочу добавить предупреждение на случай, если в имени модуля содержится слишком много идентификаторов. Сначала я задам класс действий:
class Module::Name::Actions { method TOP($/) { if $.elems > 5 { warn 'В имени модуля слишком много идентификаторов – может, укоротишь?.. '; } } }
Обычное такое определение класса в Perl 6. Я добавил метод TOP, совпадающий с первым токеном грамматики. Затем я подсчитываю количество совпадений, и если их больше 5, выдаю предупреждение. Выполнение оно не прерывает, но даёт понять пользователю о том, что стоит задуматься над переименованием модуля.
Затем инициализируем класс действий и передадим его в parse как аргумент:
my $actions = Module::Name::Actions.new; my $match_obj = Legal-Module-Name.parse($proposed_module_name, :actions($actions));
Грамматика вызывает соответствующий метод класса действий каждый раз, когда при парсинге встретится подходящий токен. В нашем случае это произойдёт один раз во время парсинга.
Грамматики в Perl 5
И в Perl 5 можно делать грамматики. Для схожего с Perl 6 решения можно посмотреть в сторону Regexp::Grammars или Ingy Döt Net’s Pegex. Отличные реализации можно посмотреть в главе 1 "Mastering Perl" от brian d foy, где содержится пример грамматики для JSON.
ссылка на оригинал статьи http://habrahabr.ru/post/263965/
Добавить комментарий