Некоторые сведения о 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";



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