Выпуск 13. Март 2014

От редактора: Год журналу | Содержание | Сигнатура функции в Perl 5.20

«Старые» современные возможности Perl

Расмотрены наиболее интересные, по мнению автора, особенности языка Perl

Perl это вообще весьма занятный язык программирования общего назначения. Многие говорят что он очень древний, в нем что-то не поддерживается, что это вообще убогий язык и нужно бы его закопать.

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

Где-то давно я находил фразу приблизительно следующего содержания:

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

В рамках этой статьи я покажу, как можно делать замечательные и модные вещи на великолепном, хотя и старом языке.

Анонимные функции

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

Анонимная функция создается проще простого:

my $subroutine = sub {
    my $msg = shift;
    printf "I am called wit message: %s\n", $msg;
    return 42;
};
# $subroutine теперь ссылается на анонимную функцию
$subroutine->("Oh, my message!");

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

Замыкания

Как говорит нам википедия:

Замыкание — это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. В записи это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.

# возвращает ссылку на анонимную функцию
sub adder($) {
    my $x = shift;    # в котором x - свободная переменная,
    return sub ($) {
        my $y = shift;    # а y - связанная переменная
        return $x + $y;
    };
}
 
$add1 = adder(1);   # делаем процедуру для прибавления 1
print $add1->(10);  # печатает 11
 
$sub1 = adder(-1);  # делаем процедуру для вычитания 1
print $sub1->(10);  # печатает 9

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

Неизменяемый объект

Нет ничего проще. Очень часто приходится видеть простыни текстов на форумах, как это реализовать. Многие «современные», «модные» языки не могут похвастаться настолько простым и изящным решением, как то, которое имеет Perl на такой случай. Фактически, state работает как my, у него одинаковая область видимости, но при этом переменная state НИКОГДА не будет переинициализирована в рамках программы. Это иллюстрируют два примера.

use feature 'state';
gimme_another();
gimme_another();
sub gimme_another {
    state $x;
    print ++$x, "\n";
}

Эта программа напечатает 1, затем 2.

use feature 'state';
gimme_another();
gimme_another();
sub gimme_another {
    my $x;
    print ++$x, "\n";
}

А эта 1, затем 1.

Бесскобочные функции

Я думаю, что это правильный перевод parentheses less.

Очень часто print, например, пишется и вызывается без скобок. Возникает вопрос, а можем ли мы тоже создавать такие функции?

Можем. Для этого у Perl есть даже специальная прагма — subs. Синтетический пример, в котором мы хотим сделать красивую проверку. Допустим, мы хотим напечатать OK, если определенная переменная true. Я позволю себе напомнить, что в Perl нет такого типа данных, как Boolean, также, как и в C нет такого типа данных, тогда как в место него выступает не ложное значение — например, 1.

use strict;
use subs qw/checkflag/;
my $flag = 1;
print "OK" if checkflag;

sub checkflag {
    return $flag;
}

Данная программа напечатает OK. Но это не единственный способ. Perl — довольно умен, а потому, если мы реструктуризируем нашу программу и приведем ее к такому виду:

use strict;
sub checkflag {
    return $flag;
}

my $flag = 1;
print "OK" if checkflag;

То результат будет тот же. Закономерность здесь слудеющая. Мы можем вызывать функцию без скобок в нескольких случаях:

  • Используя прагму subs.
  • Написав функцию ПЕРЕД ее вызовом.
  • Использовать прототипы функций.

Рассмотрим, что же такое прототипы.

Прототипы функций

Для начала стоит выяснить, как вообще функции работают в Perl.

Есть, например, какая-то абстрактная себе функция, которая называется, допустим my_sub:

sub my_sub {
    print join ', ', @_;
}

Например, мы ее вызываем следующим образом:

my_sub(1, 2, 3, 4, 5);

Функция напечатает следующее:

1, 2, 3, 4, 5, 

Таким образом мы можем передавать в ЛЮБУЮ функцию Perl любое количество аргументов. И пусть сама функция разбирается, что мы от нее хотели.

В функцию передается «текущий массив», контекстная переменная. Поэтому, запись вида:

sub ms {
    my $data = shift;
    print $data;
}

Эквивалентна:

sub ms {
    my $data = shift, @_;
    print $data;
}

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

Функция Perl с прототипами будет выглядеть так:

sub my_sub($$;$) {
    my ($v1, $v2, $v3) = @_;
    $v3 ||= 'empty';
    printf("v1: %s, v2: %s, v3: %s\n", $v1, $v2, $v3);
}

Прототипы функций записываются после имени функции в круглых скобках. Прототип $$;$ означает, что в качестве параметров ОБЯЗАНЫ присутствовать два скаляра и третий по желанию, ; — отделяет обязательные параметры от возможных.

Теперь, если мы ее попробуем вызвать вот так:

my_sub();

То мы получим ошибку вида:

Not enough arguments for main::my_sub at pragmaticperl.pl line 7, near "()"
Execution of pragmaticperl.pl aborted due to compilation errors.

Однако, если вызвать вот так:

&my_sub();

То проверка прототипов не будет происходить.

Важный момент. Прототипы будут работать в следующих случаях:

  • Если функция вызывается без знака амперсанда (&). Perlcritic, кстати, ругается на запись вызова функции через амперсанд.
  • Если функция написана ПЕРЕД вызовом. Если мы сначала вызовем функцию, а потом ее напишем, при включенных warnings получим следующее предупреждение:

    main::my_sub() called too early to check prototype at pragmaticperl.pl line 4.

Правильная программа с прототипами выглядит так:

use strict;
use warnings;
use subs qw/my_sub/;

sub my_sub($$;$) {
    my ($v1, $v2, $v3) = @_;
    $v3 ||= 'empty';
    printf("v1: %s, v2: %s, v3: %s\n", $v1, $v2, $v3);
}
my_sub();

ООП

OOП у Perl странное. Кто-то говорит, что его нет, а кто-то говорит, что это лучшая реализация, т.к. она не мешает жить программисту, а позволяет делать сложные вещи возможными. Я для себя, например, еще не определился.

Что такое ООП в случае Perl? Это, так называемый, blessed hashref. Инкапсуляции, как таковой, в Perl нет. Есть сторонние модули, например, Moose, Mouse, которые предоставляю все возможности ООП Perl программисту ценой потери скорости.

Итак, простейший пакет.

use strict;
use Data::Dumper;
my $obj = Top->new();
print Dumper $obj;

$obj->top_sub();

package Top;
use strict;
sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    return $self;
}

sub top_sub {
    print "I am TOP sub\n";
}
1;

Данная программа выведет:

$VAR1 = bless( {}, 'Top' );
I am TOP sub

Собственно, ООП сводится к тому, что есть объект и он может выступать хранилищем состояний и вызывать методы пакета. Через стрелочку ($obj->top_sub).

Множественное наследование

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

ваш код сегодняшний коллега напоминает даунхилл среди деревьев и говнища велосипед и костыли

Но, Perl нам позволяет сделать и это. Далее приведен пример программы, которая наследует два класса:

use strict;
use Data::Dumper;

my $obj = Bottom->new();
print Dumper $obj;
# вызываем функции всех пакетов
$obj->top_sub();
$obj->top2_sub();
$obj->bottom_sub();

package Top;
use strict;
sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    return $self;
}

sub top_sub {
    print "I am TOP sub\n";
}

package Top2 {
    sub new {
        my $class = shift;
        my $self = {};
        bless $self, $class;
        return $self;
    }

    sub top2_sub {
        print "I am TOP2 sub\n";
    }    
}

package Bottom;
use strict;
# наследуем Top и Top2
use base qw/Top Top2/;

sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    return $self;
}

sub bottom_sub {
    print "I am BOTTOM sub\n";
}

1;

В данном случае программа выведет:

I am TOP sub
I am TOP2 sub
I am BOTTOM sub

Оверрайд методов

Оверрайд довольно часто хорошая штука. Например, у нас есть модуль, который писал некий N. И все в нем хорошо, а вот один метод, допустим, call_me, должен всегда возвращать 1, иначе беда, а метод из базовой поставки модуля возвращает всегда 0. Код модуля трогать нельзя.

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

use strict;
use Data::Dumper;

my $obj = Top->new();
if ($obj->call_me()) {
    print "Purrrrfect\n";
}
else {
    print "OKAY :(\n";
}

package Top;
use strict;
sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    return $self;
}

sub call_me {
    print "call_me from TOP called!\n";
    return 0;
}

1;

Которая выведет:

call_me from TOP called!
OKAY :(

И снова у нас есть решение:

Допишем перед вызовом $obj->call_me() следующую вещь:

*Top::call_me = sub {
    print "Overrided subroutine called!\n";
    return 1;
};

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

Overrided subroutine called!
Purrrrfect

Код модуля не меняли, функция теперь делает то, что нам надо.

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

Локальные переменные или динамическая область видимости:

Допустим, у нас есть скрипт, который что-то считает. Вдруг нам в функции, например, понадобилась локальная переменная, которая, пусть, должна быть копией глобальной.

Мы можем это сделать следующим образом:

use strict;
use warnings;
my $x = 1;
print "x: $x\n";
do_with_x();
print "x: $x\n";

sub do_with_x {
    my $y = $x;
    $y++;
    print "y: $y\n";
}

Вывод ожидаемый:

x: 1
y: 2
x: 1

Однако, в данном случае мы можем обойтись без y. Решение выглядит так:

use strict;
use warnings;
use vars '$x';
$x = 1;
print "x: $x\n";
do_with_x();
print "x: $x\n";

sub do_with_x {
    local $x = $x;
    $x++;
    print "x: $x\n";
}

Эта штука и называется динамической областью видимости. Очень хорошо этот пример помогает понять представление переменной в виде карточек local — это когда мы закрываем то, что написано на карточке другой карточкой, а как только выходим из блока, все возвращается на круги своя. Также очень часто эта конструкция используется внутри блоков кода в которых необходим автосброс буфера, тогда можно сделать:

local $| = 1;

Или, если лениво писать в конце каждого print \n:

local $\ = "\n";

Атрибуты функций

В Python есть такое понятие, как декоратор. Это такая штуковина, которая позволяет «добавить объекту дополнительное поведение».

Да, в Perl декораторов нет, зато есть атрибуты функций. Если мы откроем perldoc perlsub и посмотрим на описание функции, там есть такая любопытная запись:

sub NAME(PROTO) : ATTRS BLOCK

Таким образом, функция с атрибутами может выглядеть так:

sub mySub($$;$) : MyAttr {
    print "Hello, I am sub with attributes and prototypes!";
}

Многие программисты, которые пишут на Perl ни разу не сталкивались ни с атрибутами, ни с прототипами. Причина этого кроется в литературе по языку для быстрого старта довольно низкого качества и сомнительного содержания. Но мы работаем над этим.

И скоро даже что-нибудь покажем. Все вышесказанное не касается книги с Верблюдом, Альпакой и Ламой, это прекрасная литература и тем кто ее не читал — я настоятельно рекомендую. Вернемся к нашим атрибутам.

Работа с атрибутами в Perl дело нетривиальное, потому уже довольно давно в стандартную поставку Perl входит модуль Attribute::Handlers.

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

Допустим, у нас есть функция, которая может быть вызвана только в том случае, если пользователь авторизован. За то, что пользователь авторизован отвечает переменная $auth, которая равна 1, если пользователь авторизован и 0, соответственно, нет. Мы можем сделать следующим образом:

my $auth = 1;

sub my_sub {
    if ($auth) {
        print "Okay!\n";
        return 1;
    }
    print "YOU SHALL NOT PASS!!!1111";
    return 0;
}

И это нормальное решение.

Но может возникнуть такая ситуация, что функций будет становиться больше и больше. А в каждой делать проверку будет становиться накладно. Проблему можно решить на атрибутах.

use strict;
use warnings;
use Attribute::Handlers;
use Data::Dumper;

my_sub();

sub new {
    return bless {}, shift;
}

sub isAuth : ATTR(CODE) {
    my ($package, $symbol, $referent, $attr, $data, $phase, $filename, $linenum) = @_;
    no warnings 'redefine';
    unless (is_auth()) {
        *{$symbol} = sub {
            require Carp;
            Carp::croak "YOU SHALL NOT PASS\n";
            goto &$referent;
        };
    }
}

sub my_sub : isAuth {
    print "I am called only for auth users!\n";
}

sub is_auth {
    return 0;
}

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

YOU SHALL NOT PASS at myattr.pl line 18. main::__ANON__() called at myattr.pl line 6

А если мы заменим return 0 на return 1 в is_auth, то:

I am called only for auth users!

Я не зря написал про атрибуты в конце статьи. Для того, чтобы написать этот пример мы воспользовались:

  • Анонимными функциями.
  • Оверрайдом функций.
  • Специальной формой оператора goto.

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

К тому же, не стоит забывать, что они, все-таки, являются экспериментальной фичей Perl, а потому их синтаксис может меняться.

О чем хотелось бы еще написать и я напишу:

  • Фазы исполнения Perl программы.
  • Более пристальный взгляд на модули.
  • AUTOLOAD и DESTROY.
  • Особая форма оператора goto.
  • Прагмы Perl.

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

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


От редактора: Год журналу | Содержание | Сигнатура функции в Perl 5.20
Нас уже 1393. Больше подписчиков — лучше выпуски!

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