Как стать автором
Обновить
110.73
Рунити
Домены, хостинг, серверы, облака

Функции в Perl

Время на прочтение11 мин
Количество просмотров64K
image

В 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)
Теги:
Хабы:
Всего голосов 106: ↑100 и ↓6+94
Комментарии37

Публикации

Информация

Сайт
runity.ru
Дата регистрации
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Рунити