В 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 my_sub($$;$) : MyAttribute {
my ($param) = @_;
}
Где $$;$ — это прототип, а MyAttribute — это атрибут. Прототипы и атрибуты будут рассмотрены далее в статье. А мы пока рассмотрим более простой вариант записи функции:
sub my_sub {
return 1;
}
Здесь мы написали функцию, которая возвращает единицу.
Но в этой записи не указано, сколько аргументов принимает функция. Именно поэтому ничего не мешает вызвать её вот так:
my_sub('Туземец', 'Бусы', 'Колбаса', 42);
И всё прекрасно выполняется! Это происходит потому, что в Perl передача параметров в функцию сделана хитро. Perl славится тем, что у него много так называемых «специальных» переменных. В каждой функции доступна специальная переменная @_, которая является массивом входящих параметров.
Поэтому внутри функции мы можем поместить входные параметры в переменные так:
my ($param) = @_;
Это работает и в случае нескольких параметров:
my ($param1, $param2, $param3) = @_;
Очень часто в функциях пишут следующее:
sub my_sub {
my $param = shift;
...
}
Дело в том, что в Perl многие функции при вызове без аргументов используют переменные по умолчанию. shift же по умолчанию достаёт данные из массива @_. Поэтому записи:
my $param = shift;
… иmy $param = shift @_;
… совершенно эквивалентны, но первая запись короче и очевидна для Perl-программистов, поэтому используется именно она.shift можно использовать и для получения нескольких параметров, в том числе комбинируя в одно списочное присваивание:
my ($one, $two, $three) = (shift, shift, shift);
Другая запись:
my ($one, $two, $three) = @_;
… работает точно так же.А теперь внимание! Грабли, на которые рано или поздно наступает каждый Perl-программист:
sub my_sub {
my $var = @_;
print $var;
}
Если вызвать данную функцию как my_sub(1, 2, 3) в $var мы внезапно получим не 1, а 3. Это происходит потому, что в данном случае контекст переменной определяется как скалярный, а в Perl массив в скалярном контексте возвращает свой размер, а не первый элемент. Чтобы исправить ошибку, достаточно взять $var в скобки, чтобы контекст стал списочным. Вот так:
sub my_sub {
my ($var) = @_
}
И теперь, как и ожидалось, при вызове my_sub(1, 2, 3) в $var будет 1.
Кроме того, в Perl параметры передаются по ссылке. Это значит, что мы можем внутри функции модифицировать параметры, которые в неё переданы.
Например:
my $var = 5;
my_sub($var);
print $var;
sub my_sub {
# вспоминаем, что доступ к элементам массива выполняется в скалярном контексте
# т. е. доступ к нулевому элементу массива @arr будет выглядеть как $arr[0], то же самое и с
# @_.
$_[0]++; # $_[0] — первый элемент массива @_.
}
Результат будет 6. Однако в Perl можно сделать в каком-то роде «передачу по значению» вот так:
my $var = 5;
my_sub($var);
print $var;
sub my_sub {
my ($param) = @_;
$param++;
}
А вот теперь результат будет 5.
И последние два нюанса, которые очень важны. Perl автоматически возвращает из функции результат последнего выражения.
Возьмём код из предыдущего примера и немного его модифицируем:
my $var = 5;
my $result = my_sub($var);
print $result;
sub my_sub {
my ($param) = @_;
++$param;
}
Это будет работать точно так же, как если бы в последней строке функции был явный возврат значения:
return ++$param;
Функция вернёт 6.
И ещё одна особенность: если в теле функции вызывается другая функция с использованием амперсанда и без скобок, то внутренняя функция получает на вход параметры той функции, в теле которой она вызывается. Т. е. массив @_ будет автоматически передан из внешней функции во внутреннюю. Это может привести к неочевидным багам.
use strict;
use Data::Dumper;
my_sub(1, 2, 3);
sub my_sub {
&inner;
}
sub inner {
print Dumper \@_;
}
Результат:
$VAR1 = [
1,
2,
3
];
Однако, если явно указать (с помощью пустых скобок), что функция вызывается без параметров, то всё в порядке:
sub my_sub {
&inner();
}
И вывод будет выглядеть вот так:
$VAR1 = [];
Впрочем, вызовы функций с использованием амперсанда используются очень редко и в коде почти не встречаются.
Анонимные функции
Анонимные функции объявляются в месте использования и не получают уникального идентификатора для доступа к ним. При создании они либо вызываются напрямую, либо ссылка на функцию присваивается переменной, с помощью которой затем можно косвенно вызывать эту функцию.
Элементарное объявление анонимной функции в Perl:
my $subroutine = sub {
my $msg = shift;
printf "I am called with 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
Замыкания использовать полезно, например, когда необходимо получить функцию с уже готовыми параметрами, которые будут в ней сохранены. Или же для генерации функции-парсера, колбеков.
Бесскобочные функции
На наш взгляд, это самый подходящий перевод термина parenthesis-less.
Например, print часто пишется и вызывается без скобок. Возникает вопрос, а можем ли мы тоже создавать такие функции?
Безусловно. Для этого у Perl есть даже специальная прагма — subs. Предположим, нам нужна функция, проверяющая значение переменной на истинность.
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 не для жёсткого ограничения типов параметров, передаваемых функциям. Это подсказка для языка: как разбирать то, что передаётся в функцию.
Авторы из PerlMonks объясняли это как «parameter context templates» — шаблоны контекста параметров. Детали на примерах ниже.
Есть, к примеру, абстрактная функция, которая называется my_sub:
sub my_sub {
print join ', ', @_;
}
Мы её вызываем следующим образом:
my_sub(1, 2, 3, 4, 5);
Функция напечатает следующее:
1, 2, 3, 4, 5,
Получается, что в любую функцию Perl можно передать любое количество аргументов. И пусть сама функция разбирается, что мы от неё хотели.
Предполагается, что должен быть механизм контроля переданных в функцию аргументов. Эту роль и выполняют прототипы.
Функция 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 (средство статического анализа 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: %s\n", $v1, $v2, $v3);
}
my_sub();
В Perl существует возможность узнать, какой у функции прототип. Например:
perl -e 'print prototype("CORE::read")'
Выдаст:
*\$$;$
Оверрайд методов
Оверрайд — часто довольно полезная штука. Например, у нас есть модуль, который писал некий 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;
};
А ещё лучше, для временного оверрайда используем ключевое слово local:
local *Top::call_me = sub {
print "Overrided subroutine called!\n";
return 1;
};
Это заменит функцию call_me пакета Top в лексической области видимости (в текущем блоке).
Теперь наш вывод будет выглядеть так:
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:
Генерация функций на лету
Допустим, нам нужно написать множество функций, выполняющих примерно одинаковые действия. Например, набор аксессоров у объекта. Написание подобного кода вряд ли кому-то доставит удовольствие:
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. «Лень, нетерпение, надменность» (Л. Уолл).
Функции можно генерировать. В 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 my_sub($$;$) : 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 PASS\n";
};
}
}
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) и при содействии программистов REG.RU: Тимура Нозадзе (@TimurN), Виктора Ефимова (@vsespb), Полины Шубиной (@imagostorm), Andrew Nugged (@nugged)