ArininAV

Глава 6.6. Подпрограммы

6.6.1. Общее описание

Подобно многим другим языкам программирования, PERL позволяет нам писать подпрограммы. Определение подпрограммы имеет следующий вид:

sub имя БЛОК

где имя — идентификатор, задающий имя подпрограммы, а БЛОК содержит тело подпрограммы, т. е. операторы, которые выполняются при ее вызове.

Вызов подпрограммы имеет вид:

имя(список) или имя список

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

sub имя;

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

&test;

sub test {
  print 'OK';
}

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

Аргументы передаются подпрограммам в массиве @_. Если, например, мы вызываем функцию двух аргументов, то эти аргументы будут занесены в переменные $_[0] и $_[1]. Массив @_ является локальным для подпрограммы, но его элементы служат синонимами для соответствующих аргументов. Это означает, что изменение переменной $_[0] повлечет изменение первого фактического аргумента (или вызовет ошибку, если этот аргумент является константой). Если аргументом является массив или ассоциативный массив, который не существовал в момент вызова подпрограммы, то он будет создан только в том случае, если подпрограмма изменяет его или ссылается на него. Присваивание значения всему массиву @_ уничтожает его связь с фактическими аргументами.

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

В PERLе нет именованных формальных аргументов. На практике мы обычно присваиваем фактические аргументы локальным переменным, созданным функцией my(), например:

sub max {
  my $max = shift(@_);
  foreach $x (@_) {
    $max = $x if $max < $x;
  }
  return $max;
}

print max(1, 2, 3, 4, 5);

Первый оператор подпрограммы max извлекает из массива @_ его первый элемент и заносит его в локальную переменную $max. Оператор foreach сканирует остальные элементы @_ и присваивает переменной $max значение наибольшего из них. Затем подпрограмма возвращает $max вызвавшей ее программе.

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

my($key, $value) = @_;

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

sub upcase {
  return unless defined wantarray;  # пустой контекст, нечего возвращать
  my @parms = @_;
  for (@parms) { tr/a-z/A-Z/ }
  return wantarray ? @parms : $parms[0];
}

($s3, $s4) = upcase($s1, $s2);  # значения $s1 и $s2 не изменяются

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

@newlist = upcase(@list1, @list2);

Однако, не вызывайте ее так:

(@a, @b) = upcase(@list1, @list2);

потому что весь список возвращенных значений будет присвоен массиву @a, а массив @b будет пуст.

Еще несколько слов об использовании символа & перед именем вызываемой подпрограммы. В старых версиях языка он был обязателен, но в современном PERLе его можно безболезненно опускать, если подпрограмма вызывается со списком аргументов в круглых скобках. Если же мы вызываем подпрограмму без аргументов и она не была предварительно декларирована, то символ & становится обязательным. Между прочим, вызов подпрограммы без аргументов означает, что ей передается текущее состояние массива @_, т. е. операторы &mysub; и &mysub(@_); синонимичны. Еще одно применение символа & состоит в следующем: вызов вида &mysub(…) производится без проверки прототипа функции.

6.6.2. Рекурсивные подпрограммы

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

Приведем пример функции, вычисляющей факториал целого числа (факториал числа n равен 1 * 2 * … * n):

sub factorial {
  my $n = shift;
  return ($n <= 1) ? 1 : $n * factorial($n-1);
}

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

6.6.3. Локальные переменные: функция my()

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

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

my $var;            # декларация одной переменной
my (@xxx, %yyy);    # декларация списка переменных
my $var = "start";  # декларация переменной и ее инициализация
my @xxx = @yyy;     # декларация массива и его инициализация

Как мы видим из этих примеров, декларация переменных может сопровождаться их инициализацией. Если же инициализации нет, то переменные создаются с неопределенным значением.

Локальная переменная, объявленная в составном операторе, имеет своей областью действия весь оператор, включая проверяемое условие и блок continue, например:

while (my $line = <>) {
  $line = lc $line;
} continue {
  print $line;
}

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

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

{
  my $secret_val = 0;
  sub give_me_val {
    return ++$secret_val;
  }
}
print give_me_val;  # выводит 1
print give_me_val;  # выводит 2

Здесь переменная $secret_val невидима вне содержащего ее блока. Однако, она сохраняет свое значение между вызовами функции give_me_val.

6.6.4. Локализация переменных: функция local()

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

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

local $var;            # локализация одной переменной
local (@xxx, %yyy);    # локализация списка переменных
local $var = "start";  # локализация переменной и ее инициализация
local @xxx = @yyy;     # локализация массива и его инициализация

Как мы видим из этих примеров, локализация переменных может сопровождаться их инициализацией. Если же инициализации нет, то переменные локализуются с неопределенным значением.

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

@arr = (0..5);
{
  local($arr[5]) = 6;
  while (my $e = pop(@arr)) {
    last unless $e > 3;
  }
}
print "В массиве ", scalar(@arr), " элементов: ",
  join(', ', map{defined $_ ? $_ : 'undef'} @arr), "\n";

Этот пример выведет на экран следующую строку: В массиве 6 элементов: 0, 1, 2, undef, undef, 5. Откуда здесь взялись неопределенные значения? Причина в том, что в блоке мы удалили последние элементы массива. После завершения блока локализованная переменная $arr[5] была восстановлена, что привело к расширению массива до нужной длины вставкой неопределенных элементов.

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

  1. Локализация специальных переменных и массивов (например, $_ или @ARGV), чтобы восстановить их значение при выходе из подпрограммы.
  2. Временное изменение одного из элементов массива или ассоциативного массива.
  3. Создание локальных описателей файлов и каталогов, а также локальных функций с помощью typeglob.

6.6.5. Передача параметров по ссылке

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

sub popmany {
  my @retlist = ();
  foreach my $aref (@_) {
    push @retlist, pop @$aref;
  }
  return @retlist;
}
    
@tails = popmany(\@a, \@b, \@c, \@d);

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

sub common_keys {
  my ($k, $href, %seen);
  foreach $href (@_) {
    while ($k = each %$href) {
      $seen{$k}++;
    }
  }
  return grep{$seen{$_} == @_} keys %seen;
}

@common = common_keys(\%hash1, \%hash2, \%hash3);

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

sub arrange {
  my ($ref1, $ref2) = @_;
  return (@$ref1 > @$ref2) ? ($ref1, $ref2) : ($ref2, $ref1);
}

($aref, $bref) = arrange(\@c, \@d);

С использование typeglob мы могли бы написать эту функцию так:

sub arrange {
  local (*c, *d) = @_;
  return (@c > @d) ? (\@c, \@d) : (\@d, \@c);
}

(*a, *b) = arrange(\@c, \@d);

6.6.6. Безымянные подпрограммы

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

$subref = sub БЛОК

Для вызова безымянной подпрограммы используется выражение &$subref, например:

$coderef = sub { print "Ура!\n" };
&$coderef;  # выведет "Ура!"

Основной особенностью безымянных подпрограмм является то, что они исполняются в той области действия, где созданы, а не в той, где вызваны. Такое поведение называется замыканием (closure) области действия. Приведем пример:

sub myprint {
  my $x = shift;
  return sub { my $y = shift; print "$x $y!\n"; };
}
$a = myprint("Это");
$b = myprint("А это");
&$a("вы");  # выведет "Это вы!"
&$b("я");   # выведет "А это я!"

Обратите внимание, что локальная переменная $x сохраняет значение, которое было ей передано при создании ссылки на безымянную подпрограмму. Замыкание области действия распространяется только на локальные переменные; глобальные и локализованные переменные ведут себя как обычно.

6.6.7. Прототипы

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

sub(prototype) имя БЛОК

и состоит из следующих символов, указывающих на типы параметров:

Символ Описание
$ Текущий аргумент — скаляр.
@ Передает все оставшиеся аргументы как список.
% Передает все оставшиеся аргументы как список.
& Текущий аргумент — безымянная подпрограмма.
* Текущий аргумент — скаляр или ссылка на typeglob.
\символ Текущий аргумент — ссылка на переменную типа символ
; Отделяет обязательные параметры от необязательных

Примеры прототипов и вызова подпрограмм:

sub sub0()              # sub0
sub sub1($$)            # sub1 $old, $new
sub sub2($$;$)          # sub2 123, "substr"
sub sub3(@)             # sub3 $a, $b, $c
sub sub4($@)            # sub4 ":", @arg
sub sub5(\@)            # sub5 @array
sub sub6(\%)            # sub6 %{$hashref}
sub sub7(*;$)           # sub7 HANDLE, $name
sub sub8(**)            # sub8 READHANDLE, WRITEHANDLE
sub sub9(&@)            # sub9 {/ref/} $a, $b, $c

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

sub mygrep(&@) {
  my $code = shift;
  my @result;
  foreach (@_) {
    push(@result, $_) if &$code;
  }
  @result;
}
@x = ("aaa", "bbb", "#ccc", "ddd");
@y = mygrep(sub { return !/^#/ }, @x); # @y = ("aaa", "bbb", "ddd")

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

Мы можем запросить прототип любой подпрограммы с помощью встроенной функции prototype(). Аргументом этой функции является имя подпрограммы или ссылка на нее, результатом — строка, содержащая прототип подпрограммы или неопределенное значение, если подпрограмма не имеет прототипа. Например, для приведенной выше подпрограммы mygrep вызов prototype("mygrep") вернет строку "&@".

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

sub try(&@) {
  my($try,$catch) = @_;
  eval{&$try};
  if ($@) {
    local $_ = $@;
    &$catch;
  }
}
sub catch(&) { $_[0] }

try {
  die "phooey";
} catch {
  /phooey/ and print "unphooey\n";
};

Эта программа выводит на экран строку unphooey. Если вы поняли почему, значит полностью разобрались с подпрограммами PERLа.

6.6.8. Функции-константы

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

sub pi() { 3.1415962 }
sub PI() { 4 * atan2 1, 1 }

sub FLAG_FOO()  { 1 << 8 }
sub FLAG_BAR()  { 1 << 9 }
sub FLAG_MASK() { FLAG_FOO | FLAG_BAR }
sub OPT_BAZ()   { not (0x1B58 & FLAG_MASK) }
sub BAZ_VAL() {
  if (OPT_BAZ) {
    return 123;
  } else {
    return 456;
  }
}
sub N() { int(BAZ_VAL) / 3 }

6.6.9. Переопределение встроенных функций

PERL позволяет нам переопределять встроенные функции, хотя эта возможность используется крайне редко. Для переопределения недостаточно обычной декларации новой функции; предварительно мы должны использовать директиву use subs:

use subs 'chdir', 'chroot', 'chmod', 'chown'; # имена переопределяемых функций
sub chdir { ... }
chdir $newpath;

Для обращения к встроенной функции в такой программе ее имя придется предварять квалификатором пакета CORE::, например

CORE::chdir $newpath;

6.6.10. Атрибуты подпрограмм

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

sub(prototype) имя : атрибуты БЛОК

а полная декларация подпрограммы:

sub(prototype) имя : атрибуты;

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

Таблица 6.12. Встроенные атрибуты подпрограмм
Атрибут Описание
lvalue Подпрограмме можно присваивать значение. Такая подпрограмма должна возвращать результат, допускающий присваивание.
method Подпрограмма является методом. Этот атрибут имеет смысл только в сочетании с атрибутом locked.
locked Этот атрибут имеет смысл только в многопоточной среде исполнения. Он означает, что подпрограмма (или ее первый аргумент, если это метод) будет заблокирована на период своего исполнения так, как если бы ее первым оператором была функция lock().

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

my $val;
sub canmod : lvalue {
  $val;
}
sub nomod {
  $val;
}

canmod() = 5;   # присваивает значение $val
nomod()  = 5;   # генерирует ошибку