Грамматика в программировании – это набор правил для разбора текста. Это очень полезная вещь – к примеру, грамматику можно использовать для проверки того, подчиняется ли строка текста конкретным стандартам или нет. У 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 Dot Net's Pegex. Отличные реализации можно посмотреть в главе 1 "Mastering Perl" от brian d foy, где содержится пример грамматики для JSON.

Комментарии (2)


  1. lockywolf
    02.08.2015 11:00

    Выглядит очень круто. Не знал, что по сути, регулярки можно делать настолько читаемыми.

    А откуда у вас японские скобки в коде?


    1. SLY_G
      04.08.2015 16:17

      Это перевод. В оригинале так было. Возможно, для лучшей читаемости приукрасили.