Функции в Perl
В 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: %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. Немногие современные языки могут похвастаться настолько простым и изящным решением. В 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: %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»)' выдаст:*\$$;$Локальные переменные или динамическая область видимости Допустим, у нас есть скрипт, который что-то считает. Вдруг нам в функции, например, понадобилась локальная переменная, которая должна быть копией глобальной.Мы можем это сделать следующим образом:
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: 1y: 2x: 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 — это когда мы закрываем то, что написано на карточке, другой карточкой, а как только выходим из блока, всё возвращается на круги своя. Другая аналогия от 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 «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; }; Это заменит функцию пакета 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, 3Autoload: 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: getOthergetName => *MyCoolPackage: getNamegetAge => *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: justnoxxAge: 25Other: 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 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, а потому их синтаксис может меняться.
Статья написана в соавторстве и по техническому материлу от Дмитрия Шаматрина АКА justnoxx и при содействии программистов REG.RU: Тимура Нозадзе, Виктора Ефимова, Полины Шубиной, Andrew Nugged