Выпуск 14. Апрель 2014

Pjam — сервер сборки перловых приложений | Содержание | Minilla — система подготовки дистрибутивов для CPAN

Атрибуты в Perl

Рассмотрен механизм атрибутов

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

Немного теории

Атрибуты были введены в Perl, если мне не изменяет память, в версии 5.6 как экспериментальный механизм. Более того, они и остаются экспериментальным механизмом, потому их поведение может меняться от версии к версии perl.

Основная идея атрибутов — передача состояния функциям, добавление дополнительного поведения к переменным и функциям.

goto

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

В Perl есть три формы оператора goto:

goto LABEL

goto EXPR

goto &NAME

Нас интересует только третья форма. Данная форма оператора goto называется «специальная форма goto». И это единственная форма оператора, которую можно использовать, и использование которой не является дурным тоном.

Эта форма отличается тем, что при ее использовании происходит JUMP, но не вызов процедуры, что существенно быстрее. Данный прием нашел широкое применение в разработке модулей, которые должны обрабатывать вызов несуществующих методов, в обертках, когда нужно вместо одного метода вызывать другой. В первом случае это делается при помощи AUTOLOAD. Но хочу предостеречь. Это использование goto действительно оправдано, но злоупотреблять им не стоит, потому что в результате можно получить совершенно несопровождаемый код.

В случае с AnyEvent такой код называют “callback hell”, а в случае с goto “goto hell”, соответственно.

Атрибуты непосредственно

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

Именно по этой причине в поставку Perl входит модуль Attribute::Handlers, который существенно упрощает работу с ними и добавляет много полезностей.

Мы рассмотрим этот модуль далее, но сначала я предлагаю разобраться, как же они работают на уровне языка. Attribute::Handlers, по сути, является оберткой над стандартным механизмом атрибутов.

Стандартные атрибуты

В стандартную поставку Perl входят: method, locked, lvalue и еще атрибуты для работы с потоками.

В данный момент нас интересует только lvalue-атрибут.

Например, у нас есть код, который выглядит следующим образом:

my $val;
sub canmod : lvalue {
    $val;  # or:  return $val;
}
sub nomod {
    $val;
}
canmod() = 5;   # assigns to $val
nomod()  = 5;   # ERROR

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

Атрибуты, к тому же, могут использоваться не только с функциями, но и с переменными.

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

Стандартные атрибуты это конечно хорошо, но гораздо интереснее создавать пользовательские атрибуты. Для того, чтобы это сделать, надо разобраться как они работают вообще.

Под капотом

Внутри атрибуты устроены следующим образом.

Когда во время компиляции perl встречает атрибут, он пытается сделать следующий вызов:

__PACKAGE__->MODIFY_CODE_ATTRIBUTES(\&mySub, @list_of_attributes);

Этот код выполняется на стадии BEGIN, но есть исключения, например, функция MODIFY_SCALAR_ATTRIBUTES выполняется при инициализации переменной.

Для того, чтобы добавить атрибуты, необходимо написать функцию в своем модуле, которая будет называться MODIFY_*_ATTRIBUTES, где вместо звездочки — желаемый тип.

Также есть весьма полезный модуль attributes, у которого есть функция get. Эта функция при вызове обращается к обработчику, который имеет название FETCH_CODE_ATTRIBUTES, для атрибутов функций. Вообще, атрибуты весьма интересная вещь. Я встречал несколько оправданных ее применений:

  • В Catalyst так проверяется, авторизован пользователь или нет.
  • Абстрактные классы, private-, public-, protected-переменные или методы при помощи атрибутов делаются на раз-два.
  • Экспортирование функций тоже весьма интересно делается на атрибутах.

Их очень полезно также использовать для модификации стандартного поведения. Возьмем, к примеру, tie, что позволяет изменять стандартное поведение стандартных структур данных весьма нетривиальным образом. Атрибуты делают то же самое, но выгодно отличаются тем, что изменение поведения атрибутами еще более нетривиально, а при неправильном использовании отладка кода превращается в сущий ад.

Для того, чтобы атрибут был выполнен успешно, он должен возвращать пустой список, например:

return;

Если же будет возвращено нечто другое, то это приведет к ошибке.

Позволю себе напомнить, как работает BEGIN в Perl.

#!/usr/bin/env perl
use strict;

print "After begin";

BEGIN {
    print "I AM AT BEGIN\n";
}

Результат работы этой программы:

I AM AT BEGIN
After begin%

Далее примеры кода, которые иллюстрируют вышесказанное.

Напишем свой первый атрибут

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

print "After begin\n";

mysub();

print 'Before $x init', "\n";
my $x : Hello = 1;
print 'After $x init', "\n";
print '$x: ', $x, "\n";

BEGIN {
    print "I AM AT BEGIN\n";
}

sub MODIFY_CODE_ATTRIBUTES {
    print "I am code attributes modifier!\n";
    print Dumper \@_;
    return;
}

sub MODIFY_SCALAR_ATTRIBUTES {
    print "I am scalar attributes modifier!\n";
    print Dumper \@_;
    return;
}

sub mysub : Hello {
    print "YAPH!\n";
}

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

Это программа работает следующим образом:

  1. Сначала выполняется BEGIN-блок.
  2. Затем выполняется MODIFY_CODE_ATTRIBUTES, т.к. мы добавили к функции mysub атрибут Hello. Атрибуты, как мы помним, срабатывают на BEGIN-стадии. Кстати, обратите внимание на то, что встроенные атрибуты начинаются с маленькой буквы, тогда как пользовательские атрибуты документация советует называть с большой буквы.
  3. Затем программа напечатает After begin, после чего будет исполнена функция mysub.
  4. Затем опять print, после которого будет инициализация переменной $x. А вот так выглядят атрибуты переменных. Например, переменная $x имеет такой же атрибут — Hello, но этот атрибут будет выполнен при инициализации переменной. Затем будет проинициализирована переменная и вызвана MODIFY_SCALAR_ATTRIBUTES.

Вывод программы будет выглядеть таким образом:

I AM AT BEGIN
I am code attributes modifier!
$VAR1 = [
          'main',
          sub { "DUMMY" },
          'Hello'
        ];
After begin
YAPH!
Before $x init
I am scalar attributes modifier!
$VAR1 = [
          'main',
          \undef,
          'Hello'
        ];
After $x init
$x: 1

Функция MODIFY_CODE_ATTRIBUTES вызывается примерно следующим образом:

__PACKAGE__->MODIFY_CODE_ATTRIBUTES(\$subref, @attrs);

Где attrs — список атрибутов. Например, мы можем добавить нашей функции mysub еще один атрибут, тогда её код будет выглядеть следующим образом:

sub mysub : Hello : World {
    print "YAPH!\n";
}

А вся программа, соответственно, будет выглядеть:

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

print "After begin\n";

mysub();

print 'Before $x init', "\n";
my $x : Hello = 1;
print 'After $x init', "\n";
print '$x: ', $x, "\n";

BEGIN {
    print "I AM AT BEGIN\n";
}

sub MODIFY_CODE_ATTRIBUTES {
    print "I am code attributes modifier!\n";
    print Dumper \@_;
    return;
}

sub MODIFY_SCALAR_ATTRIBUTES {
    print "I am scalar attributes modifier!\n";
    print Dumper \@_;
    return;
}

sub mysub : Hello : World {
    print "YAPH!\n";
}

А ее вывод будет немного отличаться. В частности, вызов MODIFY_CODE_ATTRIBUTES будет выглядеть вот так:

__PACKAGE__->MODIFY_CODE_ATTRIBUTES(\$mysubref, 'Hello', 'World');

А сам вывод так:

I AM AT BEGIN
I am code attributes modifier!
$VAR1 = [
          'main',
          sub { "DUMMY" },
          'Hello',
          'World'
        ];
After begin
YAPH!
Before $x init
I am scalar attributes modifier!
$VAR1 = [
          'main',
          \undef,
          'Hello'
        ];
After $x init
$x: 1

А вот пример кода, когда атрибут возвращает непустой список. Это приведет к ошибке:

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

mysub();

sub MODIFY_CODE_ATTRIBUTES {
    print "I am code attributes modifier!\n";
    print Dumper \@_;
    return ('Hello', 'World');
}

sub mysub : Hello : World {
    print "YAPH!\n";
}

А вот и сама ошибка:

I am code attributes modifier!
$VAR1 = [
          'main',
          sub { "DUMMY" },
          'Hello',
          'World'
        ];
Invalid CODE attributes: Hello : World at attrs.pl line 18.
BEGIN failed--compilation aborted at attrs.pl line 18.

С особенностями работы атрибутов на низком уровне можно ознакомиться в соответствующем разделе perldoc.

use attributes;

Помимо MODIFY_*_ATTRIBUTES, у пакета могут быть представлены методы другого типа — FETCH_*_ATTRIBUTES.

use attributes; дает нам доступ к двум методам, get и reftype. Принцип работы get можно проиллюстрировать следующим примером кода:

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
use attributes qw/get/;

mysub();

my @al =  attributes::get(\&mysub);
print Dumper \@al;

sub MODIFY_CODE_ATTRIBUTES {
    print "I am code attributes modifier!\n";
    print Dumper \@_;
    return;
}

sub FETCH_CODE_ATTRIBUTES {
    print "FETCH_CODE_ATTRIBUTES called!\n";
    print Dumper \@_;
    return ('Hello', 'World');
}
sub mysub : Hello : World {
    print "YAPH!\n";
}

Вывод программы будет выглядеть следующим образом:

I am code attributes modifier!
$VAR1 = [
          'main',
          sub { "DUMMY" },
          'Hello',
          'World'
        ];
YAPH!
FETCH_CODE_ATTRIBUTES called!
$VAR1 = [
          'main',
          sub { "DUMMY" }
        ];
$VAR1 = [
          'Hello',
          'World'
        ];

Мы можем видеть, что функция вернула нам то, что вернула FETCH_CODE_ATTRIBUTES.

Функция reftype интереснее. Всем известно, что в Perl есть такая функция как ref. Работает она примерно следующим образом:

ref {};

вернет нам HASH, т.к. это есть ссылка на хеш. Не забываем, что reftype по умолчанию не импортируется, потому мы должны писать use attributes qw/reftype/;. Предлагаю вашему вниманию следующий кусок кода.

ref {} eq reftype {} and print "EQUIVALENT!";

Этот код напечатает EQUIVALENT!

Т.е. в данном случае, получается, что две функции работают одинаково. Однако, Perl не PHP и эти функции отличаются. Вся штука в том, что reftype игнорирует пакет и возвращает примитивный тип данных по ссылке. Приведу пример:

#!/usr/bin/env perl
use strict;
use attributes qw/reftype/;
$\ = "\n";

my $hash = {};
my $pack = MyPack->new();

printf 'ref $pack: %s ref $hash: %s%s',
    ref $hash, ref $pack, "\n";

printf 'ref $pack: %s ref $hash: %s%s',
    reftype $hash, reftype $pack, "\n";

package MyPack;
use strict;

sub new {
    return bless {}, __PACKAGE__;
}

1;

Этот код напечатает:

ref $pack: HASH ref $hash: MyPack
ref $pack: HASH ref $hash: HASH

Вывод

Да, действительно, данный механизм слегка неудобен и громоздок, потому вместо него стоит использовать стандартный модуль Perl для работы с атрибутами — Attribute::Handlers, а то, как они работают внутри, необходимо, но не обязательно знать для общего развития.

Attribute::Handlers

Если раньше атрибуты позволяли нам получить несколько параметров и вызов их схематически можно было изобразить как:

__PACKAGE__->M_C_A($subref, @list_of_attrs)

то вызов Attribute::Handlers куда более практичный и может быть записан так:

__PACKAGE__->ATTR_HANDLER($package, $symbol, $referent, $attr, $data, $phase, $filename, $linenum)

Информации передается на порядок больше, что есть хорошо и дает нам возможность задавать более осмысленные атрибуты. Хорошим примером использования атрибутов является Attribute::Protected, где на их базе построена реализация инкапсуляции.

Инкапсуляция — один из трех китов ООП, вместе с полиморфизмом и наследованием. Так получилось, что ООП в Perl очень простое, но инкапсуляции как таковой нет.

А самое интересное, что нет способа сделать это проще, чем на атрибутах. Вот весь код модуля:

package Attribute::Protected;

use 5.006;
use strict;
use warnings;

our $VERSION = '0.03';

use Attribute::Handlers;

sub UNIVERSAL::Protected : ATTR(CODE) {
    my($package, $symbol, $referent, $attr, $data, $phase) = @_;
    my $meth = *{$symbol}{NAME};
    no warnings 'redefine';
    *{$symbol} = sub {
    unless (caller->isa($package)) {
        require Carp;
        Carp::croak "$meth() is a protected method of $package!";
    }
    goto &$referent;
    };
}

sub UNIVERSAL::Private : ATTR(CODE) {
    my($package, $symbol, $referent, $attr, $data, $phase) = @_;
    my $meth = *{$symbol}{NAME};
    no warnings 'redefine';
    *{$symbol} = sub {
    unless (caller eq $package) {
        require Carp;
        Carp::croak "$meth() is a private method of $package!";
    }
    goto &$referent;
    };
}

sub UNIVERSAL::Public : ATTR(CODE) {
    my($package, $symbol, $referent, $attr, $data, $phase) = @_;
    # just a mark, do nothing
}

1;
__END__

Более того, данный модуль позволяет пробрасывать в обработчики атрибутов дополнительные параметры. Если использовать атрибут примерно так:

sub mysub : Hello (one, two, three) {...};

то в этом случае обработчик получит в переменной $data ссылку на массив вида ['one', 'two', three].

Я описал в начале статьи третью форму goto только для того, чтобы ценность данного примера была оценена по достоинству. И для того, чтобы в коде примера goto не играл роль красной тряпки для быка, и мне не пришлось бы в комментариях объяснять, что это ни разу не спагетти код. Спасибо за понимание.

Если приведенный выше код не совсем очевиден, я добавлю в следующую статью описание typeglob и стадий исполнения перла.

Подводные камни

Фаза CHECK выбрана для обработки атрибутов не зря. В этой стадии таблица символов уже заполнена, и над ней можно издеваться. Однако, как известно, в mod_perl этой фазы нет, а потому тем, кто его использует придется городить костыли для использования атрибутов, или не использовать их вообще.

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

До новых встреч.

Дмитрий Шаматрин


Pjam — сервер сборки перловых приложений | Содержание | Minilla — система подготовки дистрибутивов для CPAN
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье