В Perl заложено огромное количество возможностей, которые на первый взгляд выглядят лишними, а в неопытных руках могут вообще приводить к выдаче багов. Доходит до того, что многие программисты, регулярно пишущие на Perl, даже не подозревают о полном функционале данного языка! Причина этого, как нам кажется, заключается в низком качестве и сомнительном содержании литературы для быстрого старта в области программирования на Perl. Это не касается только книг с Ламой, Альпакой и Верблюдом (“Learning Perl”, “Intermediate Perl” и “Programming Perl”) — мы настоятельно рекомендуем их прочитать.
В этой статье мы хотим подробно рассказать о маленьких хитростях работы с Perl, касающихся необычного использования функций, которые могут пригодится всем, кто интересуется этим языком.
Как работают функции Perl?
В большинстве языков программирования описание функций выглядит так:
function myFunction (a, b) {
return a + b;
}
А вызывается так:
myFunction(1, 2);
На первый взгляд всё просто и понятно. Однако вызов данной функции в таком виде:
myFunction(1, 2, 3);
… приведёт к различным ошибкам, суть которых будет сведена к тому, что в функцию передано неверное количество аргументов.
Функция в Perl может быть записана так:
sub mySub($$;$) : MyAttrubyte {
my ($param) = @_;
}
Где $$;$ — это прототип, а MyAttribute — это атрибут. Прототипы и атрибуты будут рассмотрены далее в статье. А мы пока рассмотрим более простой вариант записи функции:
sub mySub {
return 1;
}
Суть этой записи сводится к тому, что мы написали функцию, которая возвращает 1.
Но в этой записи не указано, сколько аргументов принимает данная функция. Именно поэтому ничего не мешает вызвать её вот так:
mySub('Туземец', 'Бусы', 'Колбаса', 42);
И всё прекрасно выполняется! Это происходит потому, что в Perl передача параметров в функцию
сделана хитро. Perl славится тем, что у него много «непонятных» специальных переменных.
К тому же, в Perl параметры передаются по ссылке. В каждой функции доступна специальная переменная @_, которая является массивом входящих параметров. Очень часто в функциях пишут следующее:
sub mySub {
my $param = shift;
...;
}
Дело в том, что в Perl многие функции при вызове без аргументов используют переменные по умолчанию. Shift же по умолчанию достаёт данные из массива @_. Поэтому записи:
my $param = shift;
и
my $param = shift @_;
… совершенно эквивалентны, но первая запись короче и очевидна для Perl-программистов, поэтому используется именно она.
Второй способ получения данных — присваивание списком. В Perl мы можем сделать так:
my ($one, $two, $three) = (shift, shift, shift);
Что есть обычное списочное присваивание.
Другая запись
my ($one, $two, $three) = @_;
… работает точно так же. А теперь внимание! Грабли, на которые рано или поздно наступает каждый Perl-программист:
sub mySub {
my $var = @_;
print $var;
}
Если вызвать данную функцию как mySub(1, 2, 3) в $var мы внезапно получим не 1, а 3.
Это происходит потому, что в данном случае контекст переменной определяется как скалярный (в Perl не существует типов «строка», или «число», или «файл», или что-то ещё. Это контекстно зависимый полиморфный язык для работы с текстами). Чтобы исправить ошибку, достаточно взять $var в скобки, чтобы контекст стал списочным. Вот так:
sub mySub {
my ($var) = @_
}
И теперь, как и ожидалось, при вызове mySub(1, 2, 3) в $var будет 1.
Как мы уже говорили, в Perl параметры передаются по ссылке. Это значит, что мы можем из функции модифицировать параметры, которые в неё переданы.
Например:
my $var = 5;
mySub($var);
print $var;
sub mySub {
# вспоминаем, что доступ к элементам массива выполняется в скалярном контексте
# т. е. доступ к нулевому элементу массива @arr будет выглядеть как $arr[0], то же самое и с
# @_.
$_[0]++;
}
Результат будет 6. Однако в Perl можно сделать в каком-то роде «передачу по значению» вот так:
my $var = 5;
mySub($var);
print $var;
sub mySub {
my ($param) = @_;
$param++;
}
А вот теперь результат будет 5.
И последние два нюанса, которые очень важны. Во-первых, Perl возвращает из функции результат последнего выражения.
Возьмём код из предыдущего примера и немного его модифицируем:
my $var = 5;
my $result = mySub($var);
print $result;
sub mySub {
my ($param) = @_;
++$param;
}
Функция вернёт 6.
И второй важный момент, хотя, скорее, предостережение. Потенциальная проблема звучит так: «если в теле функции вызывается другая функция с амперсандом и без скобок, то эта другая функция получает на вход параметры той функции(@_), в теле которой она вызывается».
use strict;
use Data::Dumper;
mySub(1, 2, 3);
sub mySub {
&inner;
}
sub inner {
print Dumper @_;
}
Результат:
$VAR1 = [
1,
2,
3
];
Однако, если ЯВНО указать, что функция вызывается без параметров, то всё в порядке.
sub mySub {
&inner();
}
И вывод будет выглядеть вот так:
$VAR1 = [];
Анонимные функции
Анонимные функции объявляются в месте использования и не получают уникального идентификатора для доступа к ним. При создании они либо вызываются напрямую, либо ссылка на функцию присваивается переменной, с помощью которой затем можно косвенно вызывать данную функцию.
Элементарное объявление анонимной функции в Perl:
my $subroutine = sub {
my $msg = shift;
printf "I am called with message: %sn", $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. Немногие современные языки могут похвастаться настолько простым и изящным решением. В Perl для этого используется state.
Фактически 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.
Бесскобочные функции
На наш взгляд, это самый подходящий перевод термина parenthesis-less.
Например, print часто пишется и вызывается без скобок. Возникает вопрос, а можем ли мы тоже создавать такие функции?
Безусловно. Для этого у Perl есть даже специальная прагма — subs. Предположим, что мы хотим напечатать OK, если определенная переменная true. В Perl (как и в C) нет такого типа данных, как Boolean, вместо него выступает неложное значение, например, 1.
use strict;
use subs qw/checkflag/;
my $flag = 1;
print "OK" if checkflag;
sub checkflag {
return $flag;
}
Данная программа напечатает OK. Но это не единственный способ. Perl хорошо продуман, поэтому, если мы реструктуризируем нашу программу и приведём её к такому виду:
use strict;
my $flag = 1;
sub checkflag {
return $flag;
}
print "OK" if checkflag;
… то результат будет тот же. Закономерность здесь следующая: мы можем вызывать функцию без скобок в нескольких случаях:
— используя прагму subs;
— написав функцию ПЕРЕД её вызовом;
— использовать прототипы функций.
Обратимся к последнему варианту.
Прототипы функций
Стоит пояснить, как функции работают в Perl.
Зачастую разное понимание цели этого механизма приводит к холиварам с адептами других языков, утверждающих, что «у перла плохие прототипы». Так вот, прототипы в Perl не для жёсткого ограничения типов параметров, передаваемых функциям. Это подсказка для языка: как разбирать то, что передаётся для функции.
Авторы из PerlMonks объясняли это как “parameter context templates” — шаблоны контекста параметров. Детали на примерах ниже.
Есть, к примеру, абстрактная функция, которая называется 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: %sn", $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 (средство статического анализа Perl кода), кстати говоря, ругается на запись вызова функции через амперсанд, то есть такой вариант вызова не рекомендуется.
— Если функция написана перед вызовом. Если мы сначала вызовем функцию, а потом её напишем, при включённых warnings получим следующее предупреждение:
main::my_sub() called too early to check prototype at pragmaticperl.pl line 4
Ниже пример правильной программы с прототипами Perl:
use strict;
use warnings;
use subs qw/my_sub/;
sub my_sub($$;$) {
my ($v1, $v2, $v3) = @_;
$v3 ||= 'empty';
printf("v1: %s, v2: %s, v3: %sn", $v1, $v2, $v3);
}
my_sub();
В Perl существует возможность узнать, какой у функции прототип. Например:
perl -e 'print prototype("CORE::read")'
выдаст:
*$$;$
Локальные переменные или динамическая область видимости
Допустим, у нас есть скрипт, который что-то считает. Вдруг нам в функции, например, понадобилась локальная переменная, которая должна быть копией глобальной.
Мы можем это сделать следующим образом:
use strict;
use warnings;
my $x = 1;
print "x: $xn";
do_with_x();
print "x: $xn";
sub do_with_x {
my $y = $x;
$y++;
print "y: $yn";
}
Вывод ожидаемый:
x: 1
y: 2
x: 1
Однако в данном случае мы можем обойтись без y. Решение выглядит так:
use strict;
use warnings;
use vars '$x';
$x = 1;
print "x: $xn";
do_with_x();
print "x: $xn";
sub do_with_x {
local $x = $x;
$x++;
print "x: $xn";
}
Эта штука и называется динамической областью видимости. Очень хорошо этот пример помогает понять представление переменной в виде карточек: local — это когда мы закрываем то, что написано на карточке, другой карточкой, а как только выходим из блока, всё возвращается на круги своя. Другая аналогия от perlmonks: my — in space, local — in time. Также очень часто эта конструкция используется внутри блоков кода, в которых необходим автосброс буфера. Тогда можно сделать:
local $| = 1;
Или, если лень писать в конце каждого print n:
local $ = "n";
Оверрайд методов
Оверрайд — часто довольно полезная штука. Например, у нас есть модуль, который писал некий N. И всё в нём хорошо, а вот один метод, допустим, call_me, должен всегда возвращать 1, иначе беда, а метод из базовой поставки модуля возвращает всегда 0. Код модуля трогать нельзя.
Пусть программа выглядит следующим образом:
use strict;
use Data::Dumper;
my $obj = Top->new();
if ($obj->call_me()) {
print "Purrrrfectn";
}
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;
};
Однако для временного оверрайда мы можем воспользоваться ключевым словом local. И тогда оверрайд будет выглядеть так:
local *Top::call_me = sub {
print "Overrided subroutine called!n";
return 1;
};
Это заменит функцию пакета Top — call_me в лексической области видимости (в текущем блоке).
И теперь наш вывод будет выглядеть примерно следующим образом:
Overrided subroutine called!
Purrrrfect
Код модуля не меняли, функция теперь делает то, что нам надо.
На заметку: если приходится часто использовать данный приём в работе — налицо архитектурный косяк и вообще эта ситуация уже описывалась картинкой. Хороший пример использования — добавление дебаг-информации в функции.
Wantarray
В Perl есть такая полезная штука, которая позволяет определить, в каком контексте
вызывается функция. Например, мы хотим, чтобы функция вела себя следующим образом:
когда надо возвращала массив, а иначе — ссылку на массив. Это можно реализовать, и
к тому же очень просто, с помощью wantarray. Напишем простую программу для демонстрации:
#!/usr/bin/env perl
use strict;
use Data::Dumper;
my @result = my_cool_sub();
print Dumper @result;
my $result = my_cool_sub();
print Dumper $result;
sub my_cool_sub {
my @array = (1, 2, 3);
if (wantarray) {
print "ARRAY!n";
return @array;
}
else {
print "REFERENCE!n";
return @array;
}
}
Что выведет:
ARRAY!
$VAR1 = 1;
$VAR2 = 2;
$VAR3 = 3;
REFERENCE!
$VAR1 = [
1,
2,
3
];
Также хотелось бы напомнить про интересную особенность Perl. %hash = @аrray; В этом случае Perl построит хэш вида
($array[0] => $array[1], $array[2] => $array[3]);
Посему, если применять my %hash = my_cool_sub(), будет использована ветка логики wantarray. И именно по этой причине wanthash нет.
Autoload
В Perl одна из лучших систем управления модулями. Мало того что программист может контролировать ВСЕ стадии исполнения модуля, так ещё существуют интересные особенности, которые делают жизнь проще. Например, Autoload.
Суть Autoload в том, что когда функции в модуле не существует, Perl ищет функцию Autoload в этом модуле, и только затем, когда не находит, активируется исключение. Это значит, что мы можем описать обработчик ситуаций, когда вызывается несуществующая функция.
Например:
#!/usr/bin/env perl
use strict;
Autoload::Demo::hello();
Autoload::Demo::asdfgh(1, 2, 3);
Autoload::Demo::qwerty();
package Autoload::Demo;
use strict;
use warnings;
our $AUTOLOAD;
sub AUTOLOAD {
print $AUTOLOAD, " called! with params: ", join (', ', @_), "n";
}
sub hello {
print "Hello!n";
}
1;
Очевидно, что функций qwerty и asdfgh не существует в пакете Autoload::Demo. В функции Autoload специальная глобальная переменная $AUTOLOAD устанавливается равной функции, которая не была найдена.
Вывод этой программы:
Hello!
Autoload::Demo::asdfgh called! with params: 1, 2, 3
Autoload::Demo::qwerty called! with params:
Генерация функций на лету
Допустим, мы хотим сделать функцию, которая должна что-то возвращать. Затем функцию, которая должна возвращать что-то другое. У объекта. Getter, так сказать. Это Perl. «Лень, нетерпение, надменность» (Л. Уолл). Я думаю, что написание кода следующего вида никому не доставляет удовольствия.
sub getName {
my $self = shift;
return $self->{name};
}
sub getAge {
my $self = shift;
return $self->{age};
}
sub getOther {
my $self = shift;
return $self->{other};
}
Функции можно генерировать. В Perl есть такая штука как тип данных typeglob. Наиболее точный перевод названия — таблица имён. Typeglob имеет свой сигил(*).
Для начала посмотрим код:
#!/usr/bin/env perl
use strict;
use warnings;
package MyCoolPackage;
sub getName {
my $self = shift;
return $self->{name};
}
sub getAge {
my $self = shift;
return $self->{age};
}
sub getOther {
my $self = shift;
return $self->{other};
}
foreach (keys %{*MyCoolPackage::}) {
print $_." => ".$MyCoolPackage::{$_}."n";
}
Вывод:
getOther => *MyCoolPackage::getOther
getName => *MyCoolPackage::getName
getAge => *MyCoolPackage::getAge
В принципе, глоб — это хэш с именем пакета, в котором он определен. Он содержит в качестве ключей элементы модуля + глобальные переменные (our). Логично предположить, что если мы добавим в хэш свой ключ, то этот ключ станет доступен как обычная сущность. Воспользуемся генерацией функций для генерации данных геттеров.
И вот что у нас получилось:
#!/usr/bin/env perl
use strict;
use warnings;
$ = "n";
my $person = Person->new(
name => 'justnoxx',
age => '25',
other => 'perl programmer',
);
print "Name: ", $person->get_name();
print "Age: ", $person->get_age();
print "Other: ", $person->get_other();
package Person;
use strict;
use warnings;
sub new {
my ($class, %params) = @_;
my $self = {};
no strict 'refs';
for my $key (keys %params) {
# __PACKAGE__ равен текущему модулю, это встроенная
# волшебная строка
# следующая строка превращается в, например:
# Person::get_name = sub {...};
*{__PACKAGE__ . '::' . "get_$key"} = sub {
my $self = shift;
return $self->{$key};
};
$self->{$key} = $params{$key};
}
bless $self, $class;
return $self;
}
1;
Эта программа напечатает:
Name: justnoxx
Age: 25
Other: perl programmer
Атрибуты функций
В Python есть такое понятие как декоратор. Это такая штуковина, которая позволяет «добавить объекту дополнительное поведение».
Да, в Perl декораторов нет, зато есть атрибуты функций. Если мы откроем perldoc perlsub и посмотрим на описание функции, то увидим любопытную запись:
sub NAME(PROTO) : ATTRS BLOCK
Таким образом, функция с атрибутами может выглядеть так:
sub mySub($$;$) : MyAttr {
print "Hello, I am sub with attributes and prototypes!";
}
Работа с атрибутами в 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 PASSn";
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, а потому их синтаксис может меняться.
Статья написана в соавторстве и по техническому материлу от Дмитрия Шаматрина АКА justnoxx и при содействии программистов
Автор: Alessandra