[Перевод] Как сделать грамматику в Perl 6
Грамматика в программировании — это набор правил для разбора текста. Это очень полезная вещь — к примеру, грамматику можно использовать для проверки того, подчиняется ли строка текста конкретным стандартам или нет. У 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.