Как изменить синтаксис Perl на свой вкус

18 октября 2011

Есть такой модуль на CPAN’е, называется Modern::Perl. В настоящее время его подключение эквивалентно написанию строк «use strict» и «use warnings», а также приводит к активации еще нескольких полезных фич перла. Мне стало интересно, а можно ли пойти дальше и написать модуль, подгружающий, к примеру, Try::Tiny и Moose? Оказалось, что написать такой модуль довольно просто.

Вот что у меня получилось:

package Glamorous::Perl::Demo;
# (c) Alexandr A Alexeev 2011 | http://eax.me/
use strict;
use warnings;
use feature qw//;

use List::Util qw/max min shuffle/;
use List::MoreUtils qw/uniq/;
use POSIX qw/ceil floor/;
use Module::Locate qw/get_source/;
use Acme::Comment;
use Try::Tiny qw/try catch finally/;

my $hooked_caller;

my $str = get_source "Method::Signatures::Simple";
$str =~ s/package\s+Method::Signatures::Simple;//;
$str =~ s/sub\s+import(\W.*?=\s*)caller(\W)/sub MSS_import$1\$hooked_caller$2/s;
eval($str);

sub import {
  my $class = shift;
  my $caller = caller;
  $hooked_caller = $caller;

  warnings->import();
  strict->import();
  feature->import(':5.10', 'unicode_strings');
  Acme::Comment->import(type => 'C++');

  MSS_import($class);

  no strict 'refs';
  for (qw/max min shuffle uniq ceil floor try catch finally/) {
    *{$caller."::$_"} = \&$_;
  }
}

1;

Подключение Glamorous::Perl::Demo эквивалентно следующему куску кода:

use strict;
use warnings;
use feauture qw/:5.10 unicode_strings/;

use List::Util qw/max min shuffle/;
use List::MoreUtils qw/uniq/;
use POSIX qw/ceil floor/;

use Method::Signatures::Simple;
use Acme::Comment type => 'C++';
use Try::Tiny qw/try catch finally/;

Боюсь, у меня не получится нормально объяснить, как и почему это работает, но я постараюсь. В общем, когда вы пишите:

use Some::Module qw/aaa bbb/;

это эквивалентно:

BEGIN {
  require Some::Module;
  Some::Module->import("aaa", "bbb");
}

В коде функции import мы можем определить, из какого модуля произошел ее вызов. Ответ на этот вопрос дает функция caller(). В частности, возвращаемое этой функцией значение используется в Exporter.pm для помещения функций нашего модуля в пространство имен подгрузившего его кода. Делается это с помощью глобов, пример работы с которыми вы можете найти в строках с 33-ей по 36-ю приведенного выше модуля Glamorous::Perl::Demo.

Несложно заметить, что помимо «своих» функций мы можем поместить в пространство имен caller’а функции любых других модулей (к примеру, как я это делаю с List::MoreUtils::uniq() и другими) и даже сказать «use strict» или «use warnings» от имени вызывающего модуля. Однако с некоторыми модулями, в основном — меняющими синтаксис перла, это не срабатывает. Одним из таких модулей-исключений является Method::Signatures::Simple. Спрашивается — что делать?

Первой моей идеей было попытаться перехватить функцию caller(). Я даже нашел на StackOverflow несколько примеров, как это можно сделать (см там и тут). Проблема в том, что конкретно для функции caller() описанные приемы почему-то не работают. Кроме того, я с трудом представляю себе, как должно выглядеть тело фальшивой функции caller().

А потом я вспомнил, что пишу на интерпретируемом языке. Ей! Так я же могу прочитать код модуля, изменить его, как мне вздумается (заменив вызов caller на нужный мне код) и затем тупо сделать eval(). Именно это и делает модуль Glamorous::Perl::Demo в строках с 16-ой по 19-ю. Метод топорный, зато работает.

Ниже приведен пример скрипта, использующий Glamorous::Perl::Demo. Обратите внимание на комментарии в стиле C++, объявление функций, конструкции try-catch и given-when, а также на вывод юникод-строк (с помощью say).

#!/usr/bin/perl
use Glamorous::Perl::Demo; // Подключаем все самое нужное
/*
Сказать привет человеку с именем $name
*/
func testFunc ($name) {
  say "Привет, $name!";
}

say join ",", shuffle uniq (1,ceil(2.5),3,3,3,4);
my $temp = 123;

try {
  die "Error!";
} catch {
  say "exception catched: $_";
};

given($temp) {
  when($_ > 5) { say "temp > 5"; }
  when([321, 123]) { say "temp is 321 or 123 (won't happen)"; }
  default { say "default action (won't happen)"; }
}
testFunc("Александр");

Следует отметить, что весь этот изврат я писал чисто для демонстрации гибкости Perl. Лично меня вполне устраивает обычный его синтаксис. Да, получение аргументов в функциях сделано не «как у всех», зато я могу дать имена этим аргументам близко к тому месту в коде, где они реально используются. Да, несколько утомительно писать много разных «use» в начале скрипта, но это все равно, что жаловаться на необходимость писать много «include» в начале программ на C++. Кстати, нормальные IDE (скажем, Padre) умеют писать в начале кода «use strict» и «use warnings», а также обладают множеством других интересных возможностей (встроенный отладчик, средства рефакторинга…).

Между прочим, в крупном проекте модуль типа Glamorous::Perl::Demo может и пригодиться. Представьте — пишем одну строчку и получаем strict, warnings, Carp, IO::Handle, элементы функционального программирования, и так далее. Но тут универсального решения нет и, по всей видимости, быть не может. Например, в сообществе perl-программистов нет согласия в отношении того, следует ли пользоваться модулем Moose.

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

Как обычно, любые вопросы, замечания и дополнения приветствуются.

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.