Некоторые сведения о Perl 5/Функции и процедуры

Подпрограммы или процедуры играют ту же роль, что и в других языках программирования:

  • они позволяют разбить одну большую программу на несколько небольших частей, чтобы сделать ее понятной;
  • они объединяют операторы в единый фрагмент, который может повторно вызываться, возможно при разных входных параметрах.

В Perl нет разницы между процедурами и функциями, однако мы условимся называть функциями такие процедуры, которые возвращают некоторый результат той части программы, которая их вызывает.

Определение процедуры

править

Процедура может быть объявлена в любом месте основной программы следующим образом:

sub <имя-процедуры> [(<прототип>)] [{<основной-блок>}]

где

  • <имя-процедуры> — имя процедуры, по которому ее можно вызывать явно.
  • (<прототип>) — специальная строка, которая описывает какие парараметры разрешено передавать процедуре.
  • {<основной-блок>} — блок операторов, являющийся определением процедуры и выполняющийся при каждом ее вызове.

Подобно языку Си, процедуру можно объявить, но не определить (не предоставить основной блок), возможно потому, что ее определение находится в другом файле. В этом случае объявление должно выглядеть так

sub <имя-процедуры> [(<прототип>)];
# Это требуется, чтобы функция была зарегистрирована в таблице символов.

Тело процедуры может хранится в разных местах:

  • оно может быть определено в том же исходном файле;
  • оно может быть определено в отдельном файле и загружаться с помощью do, require и use;
  • текст процедуры может быть передан функции eval(): в этом случае компиляция процедуры будет происходить каждый раз при вызове;
  • текст процедуры может быть определен анонимно и вызываться по ссылке.

Вызов процедуры

править

Процедуру можно вызвать, указав ее имя с идентификатором типа & (т.н. старый стиль):

&example <args>;
&example (<args>);
&example;

# args - список аргументов

Если процедура вызывается со скобками после имени, причем не важно есть ли аргументы у функции на самом деле или нет, идентификатор типа & можно опускать:

example(<args>);
example();

Если перед вызовом процедура была определена или импортирована, то можно опустить и & и скобки:

sub example { return 0 }

example <args>;
example;

Если процедура анонимная, то идентификатор типа & в вызове обязателен:

$example = sub { return 0 };

&example <args>;
&example;

Процедура может использоваться в выражениях, если она что-то возвращает. По умолчанию процедура всегда возвращает последнее вычисленное значение в ее блоке. Можно указать возвращаемое значение с помощью функции return() в любой точке процедуры. Возвращать можно как простые скалярные значения, так и массивы.

Если процедура является целым файлом, то ее можно вызвать как perl-подпрограмму с помощью конструкции do <имя-файла>;. Если указываемый файл недоступен для чтения, то do возвращает неопределенное значение, а встроенной переменной $! будет присвоен код ошибки. Если файл может быть прочитан, но возникают ошибки при компиляции или во время исполнения, то do возвращает неопределенное значение, а в специальной переменной $@ будет хранится сообщение об ошибке.

Операция eval

править

Именованная унарная операция

eval <выражение>
eval { <выражение> }

рассматривает параметр <выражение> как текст программы Perl, компилирует его и, если не обнаруживает ошибок, выполняет в текущем вычислительном окружении. Если выражение передано блоком, то оно анализируется и компилируется один раз. Это удобно, так как ошибки обнаруживаются раньше. Если аргумент передается не блоком, то его ошибки будут обнаружены только во время исполнения.

Если <выражение> отсутствует, то по умолчанию используется переменная $_. Если выражение завершается успешно, то она возвращает последнее вычисленное значение внутри <выражение>.

Если <выражение> содержит синтаксические ошибки или вызывается die(), или при исполнении возникает ошибка, то eval() возвращает неопределенное значение, а в специальную переменную $@ заносится сообщение об ошибке.

Основным применением eval{} является перехватывание исключительных ситуаций (исключений), т.е. таких ошибок, которые принудительно прерывают исполнение всей программы. К исключению можно отнести, например, ситуацию деления на ноль. Это работает потому, что исключение прервет только исполнение eval{} и передаст управление вызвавшей части программы.

Иногда по логике программы вам нужно генерировать исключение. Для этого вы должны использовать функцию die().

Ключевое слово do

править

Ключевое слово do может использоваться в двух значениях:

  • как именованная унарная операция;
  • как способ вызывать сценарий Perl из другого файла, путь до которого является вычисляемым через выражение.

Именованная унарная операция do по своей природе является термом, к которому также можно прикреплять модификаторы. Данная операция возвращает последнее вычисленное в ней выражение.

Операция do обычно используется, чтобы порождать компактные многоуровневые конструкции. Ниже приведено несколько примеров.

# do может использоваться для компактного программирования процедур во время присваивания.
# В этом примере программа запрашивает число, и если оно отрицательное, то присваивает
# 0 переменной, иначе присваивает введенное значение. Обратите внимание, что вся процедура
# компактно совмещена с присваиванием.
my $value = do {
    print "Enter a number: ";
    my $input = <>;
    $input < 0 ? 0 : $input;
};

print "$value\n";

# Обычно do используется, когда некоторому блоку кода требуется модификатор.
# Следующий код работает как echo, а именно, он выводит все что вводит пользователь в бесконечном цикле.
# Цикл можно прервать, если ввести точку.
print "Enter '.' to quit\n";
do {
    $_ = <>;
    print "$_";
} until $_ eq ".\n";

Если после do указан литерал, либо литерал вычисляется, то do воспринимает его как путевое имя файла с кодом Perl. Если путь относительный, то файл будет искаться в директориях, указанных во встроенном массиве @INC. Если файл не может быть прочитан, то do возвращает undef, а встроенная переменная $! инициируется сообщением об ошибке. Если файл удается прочитать и скомпилировать, то do возвращает последнее вычисленное в нем выражение.

Чаще всего этой возможностью пользуются, чтобы строить динамичные конфигурационные файлы для Perl, которые объявляют глобальные переменные. Преимущество здесь в том, что вы можете встраивать логику в конфигурационные файлы, используя синтаксис языка Perl.

for $file ("/share/prog/defaults.rc",
           "$ENV{HOME}/.someprogrc")
{
    unless ($return = do $file) {
        warn "couldn't parse $file: $@" if $@;
        warn "couldn't do $file: $!"    unless defined $return;
        warn "couldn't run $file"       unless $return;
    }
}

Область видимости процедуры

править

Точкой определения переменной в Perl является то место, где она впервые встречается в программе. Область действия большинства переменных ограничена пакетом. Исключение составляют некоторые специальные предопределенные переменные интерпретатора perl. Пакет — это способ порождения пространства имен для части программы. Другими словами, каждый фрагмент кода относится к какому-то пакету.

Переменные, которые встречаются в процедурах, по умолчанию видны всему пакету (т.е. они обладают глобальной видимостью). Иногда это нежелательно: обычно мы хотим ограничить время жизни переменных процедур в рамках тела этих процедур, а также не хотим, чтобы процедуры влияли косвенно на уже существующие глобальные переменные.

Переменные, которые видны только конкретной процедуре, называются локальными (говорят что они лексические) и такие переменные обладают локальной видимостью (lexical scope)[1]. В Perl существует два способа порождения локальных переменных: при помощи функции my() и local().

Функция my()

править

Функция my() используется для объявления одной или нескольких переменных локальными и ограничивает область их действия:

  • процедурой/функцией;
  • блоком;
  • выражением, переданным eval();
  • исходным файлом программы, в зависимости от того, в каком месте была вызвана my().
my $var;
my ($var, @arr, %hash); # Если переменных несколько, то скобки обязательны
my $pi = 3.14159;

# Объявление и инициализацию можно совмещать
my ($pi, $exp) = (3.14159, 2.71828);

Функция local()

править

Функция local() вызывается аналогично my(), но создает не совсем локальные переменные, а временно заменяет текущие значения глобальных переменных локальными значениями внутри:

  • процедуры/функции;
  • блока;
  • выражения, переданного eval();
  • исходного файла с программой, в зависимости от того, в каком месте вызвана local().
local $var;
local ($var, @arr, %hash); # Если переменных несколько, то скобки обязательны
local $pi = 3.14159;

# Объявление и инициализацию можно совмещать
local ($pi, $exp) = (3.14159, 2.71828);

Если при вызове функции глобальная переменная существует, то ее предыдущее значение сохраняется в стеке и заменяется новым значением. После выхода переменной из области видимости процедуры/блока/функции eval() или файла, ее предыдущее значение восстанавливается из стека. Такие переменные иногда называют динамическими, а их область видимости — динамической областью видимости.

Функция my() появилась в Perl с пятой версии, позже local(), однако для создания истинно локальных переменных рекомендуется использовать именно функцию my(). Впрочем и у local() есть причины для применения.

Рекомендации по использованию my() и local()

править

Функция my() должна использоваться всегда, кроме следующих случаев, когда нужно использовать local():

  • Присваивание временного значения глобальной переменной, в первую очередь это относится к предопределенным глобальным переменным типа $_, $ARGV и другие.
  • Создание локального дескриптора файла, каталога или локального псевдонима или функции.
  • Временное изменение массива или хеш-массива. Например, так следует поступать, если нужно временно изменить переменные окружения в предопределенном хеш-массиве %ENV.

Функция our()

править

Чтобы явно обозначить пакетную область видимости переменной, используется функция our(). Данная функция только наделяет переменную видимостью пакета, создавая лексический псевдоним внутри него, поэтому она может применяться как к уже объявленным переменным, так и не объявленным (в этом случае побочным эффектом будет их объявление). Использование our() во многом аналогично описанным выше my() и local().

Использование этой функции в общем то не обязательно, так как любая переменная получает эту видимость по умолчанию. Данная функция используется в следующих ситуациях:

  • Когда включена директива use strict 'vars';, которая требует явного указания области видимости переменной (либо через our(), либо через квалифицированное имя) в любом месте, где она действует.
  • Когда переменная используется в блоках, например
    use strict 'vars';
    
    $main::example = 10;      # Переменная объявляется с квалификатором пакета
    
    {
        our $example;         # Создаем псевдоним на $main::example в пределах блока
        print $example, "\n"; # 10
    }
    
  • В частности, когда в одном пакете есть две переменные с одним именем, но разной видимостью
    use strict 'vars';
    
    $main::example = 10;
    my $example = 13;
    
    {
        our $example;         # Переменная из глобальной таблицы символов
        print $example, "\n"; # 10
    }
    
    # Локальная переменная
    print $example, "\n";     # 13
    

Функция our() наделяет переменную пакетной видимостью на протяжении всего лексического пространства (для глобальных переменных это весь исходный файл). Сравните

package one;

our $example = 10;     # До конца лексического пространства через все пакеты

print "$example\n"; # 10

package two;

print "$example\n"; # 10

и

package one;

our $example = 10;     # До конца лексического пространства через все пакеты

print "$example\n"; # 10

package two;

our $example = 12;     # От этой точки и до конца лексического пространства

print "$example\n";      # 12
print "$one::example\n"; # 10

Такое поведение отличает our() от до этого использовавшейся директивы use vars , которая позволяла использовать неквалифицированные имена только внутри текущего пакета. Помните, что с версии 5.6.0 использование use vars считается устаревшим подходом. Используйте только our().

Использовать our() также нужно всегда, когда вы печатаете с помощью форматов format. Дело в том, что форматы ожидают к моменту их вызова, что переменные, которые они используют, объявлены в том пространстве имен, в котором они определены сами. Так как strict запрещает объявление переменных без явного указания области их действия, необходимо подсказывать компилятору, где переменная объявлена с помощью our(). Например

use strict;

our ($item);
format STDOUT =
@<<<<<<<<<<
$item
.

sub printFormat {
    for our $item (qw { apple pear grapes }) {
        write;
    }
}

printFormat;

Передача аргументов в процедуру

править

Данные в процедуру передаются через её параметры. Для передачи аргументов используется специальный массив @_, в котором $_[0] – первый параметр, $_[1] – второй параметр и так далее. Такой механизм позволяет передать в процедуру произвольное число аргументов.

Массив @_ является локальным для процедуры, но его элементы — это псевдонимы действительно переданных параметров (не копии). Изменение параметров в @_ приводит к изменению действительных параметров. Таким образом, в Perl параметры фактически передаются всегда по ссылке. С версии 5.36 можно использовать новый синтаксис процедурных сигнатур, чтобы передавать параметры по значению, но этот механизм не включается по умолчанию.

Чтобы реализовать передачу по значению без использования сигнатур, вы должны создать внутри процедуры локальные переменные и скопировать в них значения из @_. Обычно это делается так

sub example {
    my ($p1, $p2, $p3) = @_;  # Параметры будут скопированы в локальные переменные
    ...
}

example (1, "a", 3.1415);

или с помощью функции shift, каждый вызов которой возвращает очередной элемент массива @_

sub testArgs_1 {
	my $arg1 = shift;
	my $arg2 = shift;

	print "arg1='$arg1', arg2='$arg2'\n";
}

testArgs_1 'one';
testArgs_1 'one', 'two';
arg1='one', arg2=''
arg1='one', arg2='two'

Передача аргументов по ссылкам

править

К сожалению массивы не могут быть просто так переданы в процедуру с сохранением их идентичности. Если аргумент является массивом или хеш-массивом, все его элементы сохраняются в @_. При передаче в подпрограмму нескольких массивов, все они будут перемешаны в одном @_:

sub example {
    my ($p1, $p2, $p3, $p4) = @_;
    print "@_", "\n";
    print $p1, $p2, $p3, $p4, "\n";
}

@a = (1, 2);
@b = (3, 4);

example(@a,  @b);

# Вывод:
# 1 2 3 4
# 1234

Передавать массивы можно одним из двух способов.

Первый подход, более старый, заключается в использовании типа typeglob. При передаче typeglob в процедуру, интерпретатор преобразует его в скаляр, который внутри процедуры может быть уточнен соответствующим идентификатором. Следующий пример демонстрирует это.

sub example {
    local (*array, *hash) = @_;
    foreach $item (@array) {
        print "$item", "\n";
    }
    foreach $key (keys %hash) {
        print "$hash{$key}", "\n";
    }
    $hash{name} = "Garry";  # Так как это ссылка на массив, его можно изменить из процедуры
}

@list = (1, 2, 3);
%person = ("name" => "Larry", "surname" => "Wall");

example (*list, *person);

foreach $key (keys %person) {
    print "$person{$key}", "\n";
}
# Вывод:
# Wall
# Garry

В этом примере в процедуру мы передаем не сами массивы, а переменные типа typeglob, которые легко выделить из @_, так как фактически они являются скалярами. Мы использовали здесь local() вместо my() потому, что typeglob представляет запись в таблице символов и поэтому не может быть локальной. Запись local (*array, *hash) = @_; создает синонимы (псевдонимы), т.е. *array фактически создает псевдоним для *list, а *hash для *person. Таким образом, любые изменения по псевдонимам будут приводить к изменениям в оригиналах.

Второй подход, более новый, связан с передачей ссылок на массивы. Ссылка является скаляром, поэтому ее легко выделить в @_. Таким образом, внутри процедуры достаточно просто применять операции разыменования ссылок. Вышеприведенный пример может быть переписан:

sub example {
    my ($array, $hash) = @_;
    foreach $item (@$array) {
        print "$item", "\n";
    }
    foreach $key (keys %$hash) {
        print "$$hash{$key}", "\n";
    }
    $$hash{name} = "Garry";
}

@list = (1, 2, 3);
%person = ("name" => "Larry", "surname" => "Wall");

example (\@list, \%person);

foreach $key (keys %person) {
    print "$person{$key}", "\n";
}

В данном случае мы скопировали ссылки в локальные переменные при помощи my(). Изменение оригинальных массивов через ссылки должна быть понятна.

Прототипы

править

Встроенные функции Perl всегда имеют строго определенный синтаксис, другими словами, синтаксический анализатор Perl проверяет как они вызываются. По умолчанию, как вызываются пользовательские процедуры никак не проверяется: например, вы можете вызвать функцию и передавать ей аргументы, даже когда она их не ожидает, либо вы можете случайно передать переменные не тех типов и никаких ошибочных действий интерпретатор Perl не выявит.

Чтобы контролировать вызов процедур на этапе компиляции, используются прототипы. Прототип процедуры/функции — это строка из списка символов, определяющая количество и типы передаваемых параметров. Например следуюшая функция

sub example ($$) {
   ...
}

имеет прототип, который говорит, что функция ожидает две скалярные переменные. Следующие символы можно использовать в прототипе, чтобы обозначить передаваемый тип:

  • $ (скаляр)
  • @ (массив)
  • % (хеш-массив)
  • & (анонимная процедура)
  • * (typeglob)

Если поставить перед символом в прототипе обратный слеш, например sub example (\$) {...}, то имя фактического параметра всегда должно начинаться с идентификатора этого типа. В этом случае внутри процедуры в массиве параметров @_ будет передаваться ссылка на фактический параметр, указанный при вызове. Это позволяет упростить передачу массивов по ссылкам, например, сравните

sub variant1 {
    my ($arr, $hash) = @_;
    print "|@$arr|$$hash{name} $$hash{surname}|\n";
}

sub variant2 (\@\%) {
    my ($arr, $hash) = @_;
    print "|@$arr|$$hash{name} $$hash{surname}|\n";
}

@list = (1, 2, 3);
%person = (name => "Larry", surname => "Wall");

variant1(\@list, \%person);  # Ссылки передаем явно
variant2(@list, %person);    # По прототипу параметры преобразуются в ссылки автоматически

Обязательные параметры отделяются от необязательных внутри прототипа символом ;. Например

sub example ($$$;$) { ... }

имеет 4 параметра, первые 3 из которых обязательны.

Следует помнить, что прототип не проверяется, когда вызов процедуры начинается с амперсанда & (&example).

Применение прототипов

править

Интерпретатор имеет довольно много запутанных правил, связанных с использованием прототипов, однако, вам достаточно помнить самые значимые из них.

Прежде всего прототипы полезны тогда, когда вы хотите обезопасить пользователя от неправильного использования функции, которые должны вызываться в новом стиле (т.е. не через разыменовывание ссылок на функции). Таким образом, не имеет смысла писать прототипы для функций, которые вызываются как методы класса (см. Объектно-ориентированное программирование в Perl).

Прототипы проверяются на этапе компиляции, поэтому функции с прототипами должны быть видимыми на этом этапе.

Ниже приведены базовые примеры с пояснениями.

sub example ($$)
example $var1, $var2
Ожидает два скаляра, либо два литерала, которые могут быть записаны в скаляры
sub example ($$;$)
example &some_func, 25
Ожидает три аргумента (скаляра), первые два из которых обязательные, а последний опционален. Обратите внимание, что в примере мы получаем скаляр в первом аргументе через вызов функции some_func
sub example (@)
example $a, $b, $c
Ожидает массив скаляров. Обратите внимание, что фактически такое объявление позволяет объявлять функции с произвольным числом аргументов
sub example ($@)
example "text", $a, $b, $c
Ожидает скаляр и массив скаляров
sub example (\@)
example @arr
Ожидает массив, который будет передан по ссылке.
sub example (\@$$@)
example @arr1, 1, 2, @arr2
Ожидает массив, который передается по ссылке, два скаляра и массив. Обратите внимание, что последний массив автоматически развернется в список из скаляров, которые он хранит, поэтому фактическое число параметров зависит от размера @arr2
sub example (\[%@])
example %hash
Разрешает передать по ссылке в первом аргументе разные типы данных. В данном примере по ссылке может быть передан массив или хеш.
sub example (*;$)
example HANDLE, $name
Ожидает обязательную typeglob-ссылку и необязательный скаляр
sub example (&@)
example { "hello" } $a, $b, $c
Этот частный случай будет рассмотрен ниже. В общем случае функция ожидает процедуру или анонимный код и массив
sub example (;$)
example 101
Функция имеет один необязательный аргумент
sub example ($;)
example $a > $b
Частный случай, используемый для объявления унарной процедуры. В данном случае мы ставим точку с запятой в конце для того, чтобы выражение, передаваемое без скобок в аргументе, было интерпретировано как терм. Т.е. вызов в примере аналогичен
example ($a > $b)
sub example ()
example
Запрещает передачу каких-либо аргументов функции
  • Идентификатор типа в прототипе в точности ожидает этот тип на этой позиции в фактическом вызове. Если идентификатор в прототипе снабжен обратной чертой, то фактически передаваемый тип должен соответствовать типу из прототипа (опционально он может быть передан из функций my, our или local), а в массиве параметров @_ на этот объект будет передана ссылка.
  • Разрешается на одной позиции строки прототипа передавать ссылки на разные типы, если перечислить их после обратной черты в квадратных скобках. Например, функцию ref можно было бы представить таким прототипом:
    sub ref (\[$@%&*])
    
  • В строке прототипа может использоваться символ +, который несет смысл аналогичный записи \[@%]. Он полезен для функций, которые ожидают ссылки на массив или хеш.
  • Случай для прототипа &@ является особым и позволяет вам создавать собственные синтаксические конструкции. Данный формат ожидает анонимный код в первой позиции, после которой не обязательно ставить разделитель аргументов. Благодаря этому, вы можете передавать лексемы, через которые, например, можно передавать имя процедуры и аргументы для неё (что-то похожее мы делали здесь, когда эмулировали конструкцию try..catch):
    sub try (&@) {
    	my($try,$catch) = @_;
    	eval { &$try };
    	if ($@) {
    	    local $_ = $@;
    	    &$catch;
    	}
    }
    sub catch (&) { $_[0] }
    
    try {
        die "throw";
    } catch {
    	print "$_\n";
    };
    
  • Если определение функции идет намного позже её объявления, прототип должен быть одинаковым как в объявлении, так и в определении, однако проверка прототипа будет происходить только после того, как компилятор найдет определение.

Сигнатуры

править

Начиная с версии 5.36, в процедурах Perl можно использовать сигнатуры, которые объявляют её формальные параметры. Это позволяет передавать значения в процедуру не по ссылке, а через их копирование (по значению), в результате чего, каждый вызов процедуры оперирует в своём теле своей копией значения параметра.

Эта возможность не включается автоматически потому, что её синтаксис частично накладывается на синтаксис прототипов. Чтобы иметь возможность ей пользоваться, нужно указать номер версии интерпретатора, либо активировать её явно:

# Указать версию интерпретатора ...
use v5.36;

# или активировать возможность явно
use feature 'signatures';

После того как возможность активируется, вы можете указать список формальных параметров в её объявлении сразу за именем процедуры.

use feature 'signatures';

sub example ($param1, $param2) {
    return $param1 + $param2;
}

print STDOUT example (2, 2), "\n";
print STDOUT example (3, 6), "\n";

Такая запись эквивалентна следующей:

sub example {
    my($param1, $param2) = @_;
    return $param1 + $param2;
}

Параметры в сигнатуре являются позиционными, т.е. в фактическом вызове процедуры должен сохраняться количественный состав передаваемых параметров. Это уже начинает напоминать вызовы в таких языках программирования как C/C++, Java и др., но в динамически типизированном языке есть большое количество скрытых правил:

use feature 'signatures';

# Закомментированные строки будут выбрасывать исключение.

sub example($param1, $param2, @param3) {
    print "$param1, $param2, @param3", "\n";
}

example (1, 5, (1, 2, 3));   # ПРАВИЛЬНО
example 1, 5, (1, 2, 3);     # МОЖНО: процедура ведет себя как списковая.

#example (1);                # НЕЛЬЗЯ: процедура ожидает 3 обязательных параметра.

example (sub {1}, 2, 3);    # МОЖНО: первому параметру будет передана ссылка на процедуру.
example ((10,20), 2, 3);    # МОЖНО, НО ЗАПУТАННО: массив (10, 20) будет развернут, поэтому фактически
                            #  вызов будет таким: example (10, 20, (2,3)).
example (1..5);             # МОЖНО: хвост автоматически размещается в массиве в конце: example (1, 2, (3,4,5))

example ((a => 'v1', b => 'v2')); # МОЖНО, НО ЗАПУТАННО: хеш автоматически развернется в массив, поэтому 
                                  #  вызов будет таким: example ('a', 'v1', ('b','v2')).

                
sub example_1 ($param1) {
    print "$param1", "\n";
}

#example_1 1, 2, 3;          # НЕЛЬЗЯ: слишком много аргументов для данной сигнатуры.

Вы можете видеть основное преимущество сигнатур: они совмещают в себе некоторые функции прототипов (контроль количества переданных аргументов) и позволяют давать входящим параметрам очевидные имена прямо в объявлении. Проверка числа аргументов выполняется не на этапе компиляции, а во время исполнения программы. При этом вы можете продолжать передавать значения по ссылкам, так как ссылка это скаляр с адресом в качестве значения:

use feature 'signatures';

sub example($ref) {
    $$ref[0] = 5;
}

@arr = (1, 2, 3);

print "Before: @arr\n";
example (\@arr);
print "After: @arr\n";
Before: 1 2 3
After: 5 2 3
  • Часть позиционных параметров в сигнатурах можно игнорировать. Это пригождается, когда не хочется ломать существующий интерфейс, потому что он уже много где используется:
    use feature 'signatures';
    
    # Чтобы игнорировать параметр в сигнатуре, нужно в его позиции поставить только сигил типа.
    
    sub example($p1, $, $p3) {
        print $p1, $p3, "\n";
    }
    
    example 1, 2, 3;   # МОЖНО: второй параметр передается, но игнорируется.
    
  • Некоторые позиционные параметры могут иметь значения по умолчанию, тогда они еще называются опциональными. Это значение будет использоваться каждый раз, когда не передаётся значения в фактическом вызове.
    use feature 'signatures';
    
    sub example($mandatory, $optional = "Doe") {
        print "$mandatory $optional", "\n";
    }
    
    example "Larry", "Wall";   # Larry Wall
    example "Larry";           # Larry Doe
    
    Все опциональные параметры всегда должны стоять за обязательными, причём при вызове нельзя пропускать опциональные параметры, которые стоят перед теми, что определены в вызове:
    use feature 'signatures';
    
    sub example($name, $surname = "Doe", $career = "Programmer") {
        print "$name $surname, $career", "\n";
    }
    
    # Не существует способа определить профессию, но не определить фамилию.
    
    example "Larry", "Wall";   # Larry Wall, Programmer
    
    Тем не менее, допустимо чтобы значения для опциональных параметров вычислялись, так как управление блоку процедуры передаётся после вычисления сигнатуры:
    use feature 'signatures';
    
    our $surname = "Wall";
    
    sub example($languages, $name, $surname = $surname, $career = "Programmer", $list = $languages) {
        print "$name $surname, $career. Languages: " . (join " and ", @$list), "\n";
    }
    
    # В следующем вызове значение для фамилии хранится в глобальной переменной,
    # а последний опциональный параметр вычисляется на основе ссылки на массив из первого аргумента.
    
    example [ qw{ C Perl } ], "Larry";   # Larry Wall, Programmer. Languages: C and Perl
    
  • Для присваивания значения по умолчанию опциональным параметрам, можно использовать операции ||= и //=. В первом случае параметру будет присвоено значение, если выражение возвращает ЛОЖЬ. Во-втором случае, если значение опущено в вызове, либо передано значение undef. Сравните:
    use feature 'signatures';
    
    sub example($param1 ||= "false", $param2 //= "undef") {
        print "$param1 $param2", "\n";
    }
    
    example;               # false undef
    example 1;             # 1 undef
    example 0;             # false undef
    example undef;         # false undef
    example 1 > 0;         # 1 undef
    
    example 1, 25;         # 1 25
    example 1, my @arr;    # 1 undef
    example 1, undef;      # 1 undef
    
  • Пустая сигнатура — это особый случай, который запрещает передавать процедуре параметры. Может использоваться для проверки корректности вызова:
    use feature 'signatures';
    
    # Запрещено что либо передавать.
    sub example() {
       # ...
    }
    
  • При использовании сигнатур, массив @_ по сути уже становится ненужным, и интерпретатор будет выводить предупреждения, если вы попытаетесь к нему обратиться.
  • Допустимо использовать сигнатуры с прототипами, так как они выполняют разную работу: прототипы проверяют качественный состав передаваемых аргументов времени компиляции, а сигнатуры объявляют формальные параметры, вычисляемые во время исполнения. Для использования сигнатуры и прототипа вместе в одном объявлении, необходимо присвоить процедуре атрибут :prototype(), в значении которого нужно записать прототип. Это необходимо, чтобы включить особую обработку проверки определений процедур. Например
    use feature 'signatures';
    
    sub example :prototype(\@$;$$\@) ($languages, $name, $surname = "Unknown", $career = "Programmer", $list = $languages) {
        print "$name $surname, $career. Languages: " . (join " and ", @$list), "\n";
    }
    
    @arr = ('C', 'Perl');
    
    example (@arr, "Larry");    # Larry Wall, Programmer. Languages: C and Perl
    

Контекст выполнения функции

править

Каждая функция может узнать в каком контексте она выполняется и в зависимости от этого отдавать результат в нужном контексте. Для этого используется функция wantarray(), которая возвращает ИСТИНУ, если функция или блок eval{} был вызван в списковом контексте, иначе ЛОЖЬ. Функция возвращает undef значение, если она была вызвана в void-контексте.

Следующий небольшой пример демонстрирует работу этой функции.

sub getFruits {
	my @arr = ('apple', 'pear', 'grapes');
	if (wantarray) {
		return @arr;
	} else {
		return \@arr;
	}
}

@result = getFruits();                 # Списковый контекст
print STDOUT "@{&getFruits}", "\n";    # Тоже списковый контекст  

$result = getFruits();                 # В скалярном контексте возвращается ссылка на массив
print STDOUT ref($result), "\n";
apple pear grapes
ARRAY

Рекурсивные вызовы

править

Язык Perl допускает, чтобы процедура/функция вызывала саму себя. Такие вызовы называются рекурсивными.

Программирование рекурсивных функций мало чем отличается от других языков программирования. Основной рекомендацией является обязательное объявление всех переменных как my() и local(), чтобы создавать новые копии переменных на каждом новом уровне вызова.

Применять рекурсивный подход следует осторожно, так как рекурсивное программирование всегда расточительно в плане ресурсов. Однако, существуют алгоритмы, в которых применение рекурсии оправдано и необходимо. Самой известной задачей, где без рекурсии не обойтись, это проход по дереву каталогов для получения перечня файлов.

sub walk {
    local (*ROOT);
    my ($root) = $_[0];
    opendir ROOT, $root;
    my (@files) = readdir ROOT;  # Получаем список файлов в каталоге
    closedir ROOT;
    for $file (@files) {
        if ($file ne "." and $file ne "..") {  # Файлы . и .. игнорируем
            $file = $root . "/" . $file;
            print "  $file\n" if (-f $file);
            if (-d $file) { # Если файл каталог
                print "$file:\n";
                walk($file);  # Вызываем рекурсивно для этого каталога
            }
        }
    }
}

walk "/etc";

Лямбда-функции

править

Выше мы говорили, что прототип &@ является особенным, причем настолько особенным, что только для него разрешается опустить запятую между первыми двумя аргументами. На таком прототипе реализовано много библиотечных функций, например, очень ходовая функция map, которая позволяет на ходу определять обработчик для каждого элемента списка. Вот пример вызова map:

# Для печати хеша.
use Data::Dumper;

# Данный обработчик просто подсчитывает, сколько раз каждая литера встречается
# в списке, подаваемом за ним.
map { $counter{$_}++ } 'a', 'b', 'c', 'a', 'c', 'c', 'a', 'd';
print Dumper \%counter;

Результат:

$VAR1 = {
          'a' => 3,
          'd' => 1,
          'c' => 3,
          'b' => 1
        };

В этом прототипе первым аргументом передается анонимная процедура (лямбда-функция) для каждого элемента списка, каждый из которых виден ей через $_. Сама функция map служит только интерфейсом и внутри себя готовит аргументы и вызывает с ними лямбда-функцию по некоторым правилам.

Похожим способом вы можете реализовать свой интерфейс и вызывать анонимные функции под свои задачи. Продемонстрируем как это работает. Для наглядности продемонстрируем это на алгоритме быстрой сортировки.

Быстрая сортировка основана на том, что на каждом раунде мы выбираем некоторый опорный элемент в массиве, а затем переносим из левой половины от него значения большие него в правую часть, а меньшие — в левую часть. Затем рекурсивно повторяем эти действия для левой половинки относительно текущего опорного элемента и для правой половинки, выбирая для каждой из них по своему опорному элементу. Эффективность сортировки зависит от того, насколько удачно выбран первый опорный элемент. В приведённом ниже алгоритме можно указать между какими индексами исходного массива нужно проводить сортировку (по умолчанию, если эти параметры опущены, массив сортируется целиком).

use strict;
# Для 'croak'.
use Carp;

sub quickSort(&@) {
    my $predicate = shift;
    my $a = shift;
    my $fromIndex = shift;
    my $toIndex = shift;

    croak 'Wrong usage of "quickSort": quickSort \&predicate, \@array [, $startIndex] [, $lastIndex]'
        if ref $predicate ne 'CODE';
    croak 'Second argument is not an "ARRAY" reference' if ref $a ne 'ARRAY';

    $fromIndex = $fromIndex || 0;
    $toIndex = $toIndex || $#$a + 1;

    # Поиск опорного элемента и перестановки на текущем раунде.
    #-----------------------------------------------------------------------------
    my ($iLeft, $iRight);
    if ($toIndex - $fromIndex <= 1) {
        return;
    } elsif ($toIndex - $fromIndex == 2) {
        if ($predicate->($a->[$fromIndex], $a->[$toIndex-1])) {
            my $d = $a->[$toIndex - 1];
            $a->[$toIndex - 1] = $a->[$fromIndex];
            $a->[$fromIndex] = $d;
        }
    } else {
        my $p = $a->[$fromIndex];
        $iLeft = $fromIndex + 1;
        $iRight = $toIndex - 1;
        while ($iLeft < $iRight) {
            while ($iLeft < $toIndex - 1 && $predicate->($p, $a->[$iLeft])) {
                $iLeft++;
            }
            while ($iRight > $fromIndex + 1 && $predicate->($a->[$iRight], $p)) {
                $iRight--;
            }
            last if ($iLeft >= $iRight);
            my $d = $a->[$iRight];
            $a->[$iRight] = $a->[$iLeft];
            $a->[$iLeft] = $d;
        }
        if ($predicate->($p, $a->[$iRight])) { 
            my $d = $a->[$iRight];
            $a->[$iRight] = $a->[$fromIndex];
            $a->[$fromIndex] = $d;
        } else {
            $iRight--;
        }
        #-----------------------------------------------------------------------------
        # Обработка правой половины интервала.
        quickSort($predicate, $a, $fromIndex, $iRight) if ($fromIndex < $iRight - 1);
        # Обработка левой половины интервала.
        quickSort($predicate, $a, $iLeft, $toIndex) if ($iLeft < $toIndex - 1);
    }
}

# Возвращает массив случайных чисел.
#
# getRandomArray [$size [, $min_value [,$max_value]]
#
#  $size - размер серии (10 по умолчанию).
#  $min_value - левая граница диапазона (0 по умолчанию).
#  $max_value - правая граница диапазона (100 по умолчанию).
#
# В скалярном контексте возвращает ссылку на массив; в списковом - массив.
#
sub getRandomArray {
    my $size = shift || 10;
    my $min_border = shift || 0;
    my $max_border = shift || 100;
    my $range = $max_border - $min_border;
    my $s = '(' . '$min_border + int(rand($range)),' x $size . ')';
    my @rand = eval $s;
    return wantarray ? @rand : \@rand;
}

my $arr = getRandomArray(20, -10, 45);

# Сортировка по увеличению значений.
print "Before sorting: @$arr\n";
quickSort { $_[0] >= $_[1] } $arr;
print "After sorting: @$arr\n\n";

# Предикат можно передать по ссылке.
my $reverse_order = sub { $_[0] <= $_[1] };

# Сортировка по убыванию значений.
print "Before sorting: @$arr\n";
quickSort { &$reverse_order } $arr;
print "After sorting: @$arr\n\n";

# Сортировать можно по произвольному диапазону
print "Before sorting: @$arr\n";
quickSort { $_[0] >= $_[1] } $arr, 0, 11;   # сортировка с 0 по 10 индекс.
print "After sorting: @$arr\n\n";

# Так как предикат изменяемый, нам в общем то все равно что сортировать.
# Можем сортировать и строки.
my $string_array = [ 'ccc', 'aba', 'aaa', 'ddd', 'bbb' ];

print "Before sorting: @$string_array\n";
quickSort { $_[0] ge $_[1] } $string_array;
print "After sorting: @$string_array\n\n";

Ниже показан результат одного из вызовов:

Before sorting: 22 16 38 -5 -9 7 10 25 18 26 -6 18 35 32 34 35 40 -5 44 35
After sorting: -9 -6 -5 -5 7 10 16 18 18 22 25 26 32 34 35 35 35 38 40 44

Before sorting: -9 -6 -5 -5 7 10 16 18 18 22 25 26 32 34 35 35 35 38 40 44
After sorting: 44 40 38 35 35 35 34 32 26 25 22 18 18 16 10 7 -5 -5 -6 -9

Before sorting: 44 40 38 35 35 35 34 32 26 25 22 18 18 16 10 7 -5 -5 -6 -9
After sorting: 22 25 26 32 34 35 35 35 38 40 44 18 18 16 10 7 -5 -5 -6 -9

Before sorting: ccc aba aaa ddd bbb
After sorting: aaa aba bbb ccc ddd

Интерфейсом, как несложно догадаться, является процедура перестановки. Чтобы не зависеть от типа данных внутри массива, мы должны предоставить функции предикат (лямбда-функцию), которому передаются сравниваемые элементы на некотором этапе алгоритма. В зависимости от направления сортировки, предикат должен сказать какой из сравниваемых элементов больше/меньше другого, чтобы выполнить очередную перестановку. В наших примерах мы сортировали массив чисел и массив литералов, используя встроенные операции, но в принципе вы можете закладывать более сложные процедуры в предикат. Наш массив сортируется на месте, т.е. вы должны передавать ссылку на него.

Этот пример демонстрирует только один из способов применения приёма. Данный прием можно использовать для реализации фильтрации элементов по некоторому условию; можно реализовывать преобразователи массивов, конвертеры с условиями и другое, на что хватит вашей фантазии.

Примечания

править
  1. В языке Perl термин лексическая видимость немного шире термина локальный, с которым вы можете сталкиваться в других языках программирования. Если в других языках под локальной переменной обычно понимается ограничение её видимости в пределах обозначенного блока машинных инструкций, в Perl лексическая видимость ближе к лингвистике, и больше относится к таблице символов (аналог словаря в естественном языке), в которой хранится её определение и которую видно из некоторого другого участка исходного кода. Например, если переменная хранится в некотором пакете, то она имеет лексическую видимость по отношению к этому пакету. Если к переменной можно обратиться из любой части кода в некотором исходном файле, в том числе по полному квалифицированному имени, то для этого файла переменная тоже имеет лексическую видимость. Ниже будет показано, как можно объявить переменную, которая будет видна только в блоке, тогда она будет иметь лексическую видимость по отношению к блоку. Тем не менее, мы будем использовать термин локальный наравне с термином лексический, из-за похожего смысла и распространенности именно термина локальный.