Функции в Perl

в 11:56, , рубрики: perl, Блог компании REG.RU
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 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;
	}

Функции в Perl
Если вызвать данную функцию как 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.

Зачастую разное понимание цели этого механизма приводит к холиварам с адептами других языков, утверждающих, что «у перла плохие прототипы». Так вот, прототипы в 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:

Генерация функций на лету

Функции в Perl
Допустим, мы хотим сделать функцию, которая должна что-то возвращать. Затем функцию, которая должна возвращать что-то другое. У объекта. 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!
Функции в Perl
Не зря атрибуты представлены в конце статьи. Для того чтобы написать этот пример, мы воспользовались:

— анонимными функциями;
— оверрайдом функций;
— специальной формой оператора goto.

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

Статья написана в соавторстве и по техническому материлу от Дмитрия Шаматрина АКА justnoxx и при содействии программистов REG.RU: Тимура Нозадзе, Виктора Ефимова, Полины Шубиной, Andrew Nugged

Автор: Alessandra

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js