Некоторые сведения о 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). В 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().

Передача аргументов в процедуру[править]

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

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

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

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";
    };
    
  • Если определение функции идет намного позже её объявления, прототип должен быть одинаковым как в объявлении, так и в определении, однако проверка прототипа будет происходить только после того, как компилятор найдет определение.

Контекст выполнения функции[править]

Каждая функция может узнать в каком контексте она выполняется и в зависимости от этого отдавать результат в нужном контексте. Для этого используется функция 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";



← Ссылки Пакеты, библиотеки и модули →