Next Previous Contents

Unix Review Column 5 -- Подпрограммы

Randal Schwartz

Ноябрь 1995

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

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

Давайте возьмём простую задачу. У вас имеется массив чисел в @data, и вам хочется узнать сумму всех этих чисел. Вы могли бы написать что-то вроде:


        ... code ...
        $sum = 0;
        foreach (@data) {
                $sum += $_;
        }
        # now use $sum

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


        sub sum_data {
                $sum = 0;
                foreach (@data) {
                        $sum += $_;
                }
        }

И, когда нам захочется установить $sum равной текущему значению @data, мы просто вызовем подпрограмму:


        &sum_data();

Теперь, я могу набрать этот текст, чтобы занести сумму @data в переменную $sum в любом месте программы, и повторно использовать подпрограмму в разных частях программы.

Результат сохраняется в переменной $sum. Тем не менее, каждый вызов так же возвращает занчение, так как технически вызов процедуры всегда внутри некоторого выражения (может считаться выражением). (В этом случае значение выражения выбрасывается). ``Возвращаемое значение'' подпрограммы это то, что было вычислено внутри функции в последний раз. Так получается, что последнее вычисленное значение внутри функции это всегда строка $sum += $_, результатом этого выражения является как раз $sum!

Теперь мы можем переписать вызов подпрограммы так:


        $total = &sum_data();

и $total всегда будет равняться $sum. Или даже:


        $two_total = &sum_data() + &sum_data();

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


        $two_total = 2 * &sum_data();

Если вы можете сказать, что $sum является возвращаемым значением &sum_data(), то вы можете поставить $sum последним вычисляемым значением:


        sub sum_data {
                $sum = 0;
                foreach (@data) {
                        $sum += $_;
                }
                $sum; # return value
        }

Заметьте, что $sum как вычисляемого выражения достаточно, чтобы это было последним вычисленным значением внутри подпрограммы и, таким образом, возвращаемым значением подпрограммы.

Эта подпрограмма, конечно, интересна, но она ограничена вычислением суммы значений массива @data. А что делать, если у нас есть массивы @data_one и @data_two? Нам придётся написать различные версии &sum_data() для каждого массива. Хорошо, что это необязательно. Так же, как подпрограмма может возвращать значение, она может и принимать список значений в качестве аргументов или параметров.


        $total = &sum_this(@data);

В этом случае, значения @data собираются в новый массив, называемый @_ внутри подпрограммы.


        sub sum_this {
                $sum = 0;
                foreach (@_) {
                        $sum += $_;
                }
                $sum;
        }

Заметьте, что всё, что я сделал, это заменил @data на @_, в котором содержатся передаваемые параметры. Значения, передаваемые в подпрограмму, могут быть сконструированы из любого списка. Например, я могу написать:


   
        $more_total = &sum_this(5,@data);

Число 5 будет предшествовать массиву @data, создавая массив в @_ на один элемент больше.

Подпрограмма &sum_this теперь весьма полезна. Тем не менее, что если я использую переменную $sum каким-либо образом в другой части моей программы? По умолчанию, все переменные внутри подпрограммы ссылаются на глобальную область памяти, так что подпрограмма &sum_this будет уничтожать предыдущее значение $sum. Чтобы это исправить, я могу (и должен) сделать $sum локальной переменной подпрограммы.


   
        sub sum_this {
                my $sum = 0;
                foreach (@_) {
                        $sum += $_;
                }
                $sum;
        }

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

Если вы всё ещё не обновили Perl до версии 5 (выпущен около года назад

Примерно в 1996г. -- Прим. переводчика.
, но удивительно, не все ещё программы на него переведены), вы можете qиспользовать конструкцию Perl версии 4, называемую ``local'', которая выполняет похожую функцию.


   
         sub sum_this {
                local($sum) = 0;
                foreach (@_) {
                        $sum += $_;
                }
                $sum;
        }

Тем не менее, если эта подпрограмма (с ``local'' вместо ``my'') вызывает другую подпрограмму, то все ссылки из этой подпрограммы на переменную $sum будут вести на $sum созданную ``local'', а не на глобальную переменную $sum, что может приводить, мягко говоря, к неожиданным последствиям.

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


   
        sub sum_data { &sum_this(@data); }

Я делаю это время от времени, переписывая специфические подпрограммы в обобщённые.

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


   
         sub sum_this {
                my $sum = 0;
                my @sums;
                for (@_) {
                        $sum += $_;
                        push(@sums,$sum);
                }
                @sums;
        }

Что же здесь происходит? Я создаю новый массив, называемый @sums, который содержит увеличивающийся результат сложения каждого элемента с суммой. Когда каждая сумма вычислена, она кладётся в конец списка @sums. После завершения цикла значение @sums возвращается из подпрограммы. Это означает, что я могу вызвать подпрограмму так:


   
         @result = &sum_this(1,2,3);
        print "@result\n"; # prints "1 3 6\n"

Что случится, если я вызову подпрограмму в скалярном контексте (например, присвоение результата скаляру)? Положим, скалярный контекст дойдёт до последнего вычисленного значения, в нашем случае до @sums. Значение массива в скалярном контексте есть количество элементов массива, так мы получим:


   
        $what = &sum_this(1,2,3);
        print $what;

напечатает ``3'' --- количество элементов возвращаемого значения. Небольшой фокус позволит нам скомбинировать два типа подпрограммы в один:


   
        sub sum_this {
                my $sum = 0;
                my @sums;
                for (@_) {
                        $sum += $_;
                        push(@sums,$sum);
                }
                if (wantarray) {
                        @sums;
                } else {
                        $sum;
                }
        }

В таком виде, если подпрограмма вызывается в списковом контексте (присвоение массиву, например), внутреннее значение ``wantarray'' истина, и возвращается массив @sums. Если нет, то внутреннее значение ``wantarray'' ложь, и возвращается $sum.

Так мы получаем результат вроде:


   
        $total = &sum_this(1,2,3); # gets 6
        @totals = &sum_this(1,2,3); # gets 1,3,6

Очевидно, разработчик подпрограмм имеет огромные возможности гибкости. Если вы пишите подпрограмму общего назначения для других, то убедитесь, что делает ваша подпрограмма в скалярном и списковом контексте, и, если имеет смысл, используйте конструкцию ``wantarray'', для возвращения правильного значения.

Подобно большинству алгоритмических языков, Perl поддерживает рекурсивные подпрограммы. Это означает, что подпрограмма может вызывать саму себя для выполнения части задачи. Классическим примером рекурсивной подпрограммы является вычисление чисел Фиббоначи, определённое так:


   
        F(0) = 0;
        F(1) = 1;
        F(n) = F(n-1)+F(n-2) for n > 1;

Это определение может быть преобразовано напрямую в функцию Perl:


   
        sub F {
                my ($n) = @_;
                if ($n == 0) {
                        0;
                } elsif ($n == 1) {
                        1;
                } else {
                        &F($n - 1) + &F($n - 2);
                }
        }

Эта подпрограмма на самом деле выдаст правильный результат. Для больших значений $n подпрограмма будет последовательно вызываться со значениями чисел, меньшими, чем $n. Например, вызов F(10) будет вызывать F(9) и F(8). При этом, вызов F(9) в свою очередь вызовет F(8) и так далее.

Быстрое решение состоит в том, чтобы держать кеш уже вычисленных за предыдущие вызовы значений. Назовём этот кеш @F_cache и используем так:


        sub F {
                my ($n) = @_;
                if ($n == 0) {
                        0;
                } elsif ($n == 1) {
                        1;
                } elsif ($F_cache[$n]) {
                        $F_cache[$n];
                } else {
                        $F_cache[$n] =
                                &F($n - 1) + &F($n - 2);
                }
        }

Теперь, если в функцию передаются число большее 1, происходит одно из двух: (1) если число уже было вычислено, мы просто возвращаем вычисленное значение; (2) если число ещё не было вычислено, мы вычисляем значение и запоминаем его для возможного будущего использования. Заметьте, что присвоение элемента массива @F_cache так же возвращает значение.

Я использовал такую технику на многих подпрограммах, которые имеют большую трудоёмкость вычисления. Например, сопоставление IP

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


        sub ip_to_name {
                if ($ip_to_name{$_[0]}) {
                        $ip_to_name{$_[0]};
                } else {
                        $ip_to_name{$_[0]} =
                                ... calculations ...
                }
        }

Здесь по первому параметру $_[0] как по ключу мы смотрим значение в ассоциативном массиве. Если для этого ключа есть значение, то оно возвращается немедленно, иначе вычисляется новое и запоминается для будущего использования. Такой вид кэширования ускоряет работу только когда подпрограмма одинаково вызывается во многих местах с одними и теми же аргументами, иначе вы просто потеряете время.

Я надеюсь, вы получили удовольствие от этой маленькой экскурсии в подпрограммы. В следующий раз я, возможно, поговорю о чём-нибудь ещё.


Next Previous Contents