Next Previous Contents

Unix Review Column 8

Randal Schwartz

Май 1996

В моей предыдущей заметке я ввел понятие ``ссылки'' в Perl, подобной указателю в C. Я говорил о ссылках на скалярные значения, массивы и ассоциативные массивы (хеши). В этой заметке мы поговорим о ссылках на подпрограммы, и использовании ссылок для внесения изменений в таблицу символов Perl во время выполнения, для создания алиасов.

Ссылка на подпрограмму может быть создана с помощью оператора ``создать-ссылку-на'', обратный слеш (одно, из примерно 17 значений обратного слеша):


        sub wilma {
                print "Hello, @_!";
        }
        $ref_to_wilma = \&wilma;

В этом примере, определяется подпрограмма &wilma, а затем создается ссылка на эту подпрограмму и сохраняется в переменной $ref_to_wilma. Однако необязательно определять подпрограмму для взятия ссылки на нее.

Переменная $ref_to_wilma может быть использована там где вызывается подпрограмма wilma, хотя мы должны ``разименовать (dereference)'' ее тем же способом, как и остальные ссылки. Синтаксические правила являются теми же -- заменить имя ``wilma'' в выражении &wilma на {$ref_to_wilma} (или $ref_to_wilma, поскольку это скалярная переменная), как в данном примере:


        &wilma("fred"); # выдает "hello to fred"
        &{ $ref_to_wilma }("fred"); # тоже самое
        &$ref_to_wilma("fred"); # и здесь тоже самое

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


        sub add { $_[0]+$_[1]; }
        sub subtract { $_[0]-$_[1]; }
        sub multiply { $_[0]*$_[1]; }
        sub divide { $_[0]/$_[1]; }

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


        print "enter operator op1 op2\n";
        $_ = <STDIN>;
        ## break the result on whitespace:
        ($op,$op1,$op2) = split;
        if ($op eq "+") {
                $res = &add($op1,$op2);
        } elsif ($op eq "-") {
                $res = &subtract($op1,$op2);
        } elsif ($op eq "*") {
                $res = &multiply($op1,$op2);
        } else { # divide, we hope
                $res = &divide($op1,$op2);
        }
        print "result is $res\n";

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


        ## инициализация таблицы операторов
        %op_table = (
                "+" => \&add,
                "-" => \&subtract,
                "*" => \&multiply,
                "/" => \&divide,
        );
        print "enter operator op1 op2\n";
        $_ = <STDIN>;
        ## break the result on whitespace:
        ($op,$op1,$op2) = split;
        ## get reference:
        $sub_ref = $op_table{$op};
        ## and now evaluate
        $res = &{$sub_ref}($op1,$op2);
        print "result is $res\n";

Сначала переменная $op используется как ключ в хеше %op_table, выбирая одну из четырех ссылок на подпрограммы и помещая ее в переменную $sub_ref. Затем эта ссылка разименовывается с передачей двух операндов. Это возможно, поскольку все четыре подпрограммы имеют одно количество операндов. Если бы у нас были некоторые отклонения, то мы могли бы иметь проблемы.

Однако мы можем сократить шаги по поиску и выполнению подпрограмм, например написав такой код:


        $res = &{$op_table{$op}}($op1,$op2);

что просто выполняет поиск и разыменование одним действием. Ловко?

Аналогично анонимным спискам и анонимным хешам, я могу создать анонимную подпрограмму. Hапример, вернемся к чему-то подобному подпрограмме &wilma,


        $greet_ref = sub {
                print "hello, @_!\n";
        };

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


        &$greet_ref("barney"); # hello, barney!

Одно из преимуществ анонимных подпрограмм заключается в том, что они могут быть использованы в тех местах, где использование подпрограмм с именами может казаться немного глупым. Hапример, в предыдущем примере имена &add, &subtract, &multiply, &divide были скорее капризом. При добавлении операторов, я должен был сохранять именование подпрограмм, даже хотя имена были использованы только в одном месте -- в таблице %op_table. Таким образом, используя анонимные подпрограммы, я могу полностью избежать использования имен:


        ## инициализация таблицы операторов
        %op_table = (
                "+" => sub { $_[0]+$_[1] },
                "-" => sub { $_[0]-$_[1] },
                "*" => sub { $_[0]*$_[1] },
                "/" => sub { $_[0]/$_[1] },
        );

и в действительность. функции в %op_table работают также как и раньше, только за тем исключением, что я не должен пытать себя придумыванием имен четырем подпрограмм. Это очень помогает при сопровождении -- например, для того, чтобы добавить возведение в степень (используя **), все что я должен сделать -- добавить запись в таблицу %op_table:


                "**" => sub { $_[0]**$_[1] },

вместо того, чтобы сначала создать именованную процедуру, а затем добавлять ссылку на нее в %op_table.

Ссылки на подпрограммы также полезны при передаче данных в подпрограммы. Hапример, предположим, что я написал подпрограмму, которая отбрасывает пустые строки до тех пор, пока мы не получим что-нибудь полезное, а затем возвращает полезную информацию. В качестве первого аргумента я мог бы получать подпрограмму, которая определяла бы как ``получить следующий объект''. Иногда, это может быть ``считать из файлового дескриптора'', а в другое время, это могло бы быть ``следующий элемент массива''. Вот как это могло бы выглядеть:


        $next = &non_blank(
                sub { <STDIN>; }
        ); # read from stdin
        $next = &non_blank(
                sub { shift @cache; }
        }; # grab from list @cache

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


        sub non_blank {
                my($scanner) = @_;
                my($return);
                {
                        $return = &{$scanner}();
                        redo until $return =~ /\S/;
                }
                $return;
        }

В этом примере, подпрограмма, ссылка на которую находится в переменной $scanner вызывается до тех пор, пока ее возвращаемое значение (сохраняемое в переменной $return) не будет непустым. Когда она запускается с подпрограммой содержащей <STDIN>, то происходит построковое считывание со стандартного ввода. Когда, она запускается с сдвигом элементов в массиве @cache, то мы каждый раз будем получать данные из массива @cache.

К сожалению, в процессе тестирования этого кода, я обнаружил одну проблему. Иногда, не существует непустых данных в потоке сканируемом подпрограммой &non_blank, и таким образом эта подпрограмма зацикливается. Ох! Подумав, мы приходим к логичной модификации и это решает нашу проблему. Я буду возвращать неопределенное значение, если нет больше сканируемых элементов, как в данном коде


        sub non_blank {
                my($scanner) = @_;
                my($return);
                {
                        $return = &{$scanner}();
                        last unless defined $return;
                        redo until $return =~ /\S/;
                }
                $return;
        }

Вот. Это работает! Теперь, если моей программе требуется сканирование <STDIN>, массива @cache или даже вызова другой подпрограммы для получения следующей непустой строки, то не имеет значения как это делается. И эта ошибка исправляется в одном месте, а не во всех участках кода, которые реализуют аналогичные алгоритмы.

Между прочим, я увлекся пунктуацией -- давайте упростим запись до:


        $return = &$scanner();

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


        *fred = *barney;

Здесь мы сообщаем, что символ ``fred'' является алиасом для символа ``barney''. Мы называем это ``glob''-ссылкой, поскольку она модифицирует глобальную таблицу символов.

После того, как мы это сделаем, каждое использование barney как переменной, может быть заменено на fred:


        $barney = 3;
        $fred = 3; # тоже самое
        @barney = (1,2,4);
        print "@fred"; # выдает "1 2 4"
        %fred = ("a" => 1);
        print $barney{"a"}; # выдает 1

Таким же образом создаются алиасы для подпрограмм, файловых дескрипторов, дескрипторов каталогов и имен форматов.

Мы можем быть более избирательны, передавая glob-присвоению данные о типе ссылки:


        *fred = \&wilma;

Сейчас $fred также остается $barney, @fred остается @barney, %fred остается %barney, но &fred является алиасом для &wilma. Вы часто можете увидеть такой код внутри блоков с локальной операцией glob:


        *fred = *barney;
        {
                local(*fred) = \&wilma;
                &fred(3,4,5); # &wilma(3,4,5)
        }
        &fred(6,7,8); # &barney(6,7,8)

Локализованное glob-присвоение эффективно только внутри блока кода, когда мы выходим из области видимости блока, как по волшебству восстанавливается предыдущее glob-присвоение.

Мы можем переписать вышеприведенную подпрограмму &non_blank используя локальный алиас, вместо явного разименования ссылки:


        sub non_blank {
                local(*scanner) = @_;
                my($return);
                {
                        $return = &scanner();
                        last unless defined $return;
                        redo until $return =~ /\S/;
                }
                $return;
        }

Заметьте, что мы можем вызывать &scanner, вместо неуклюжей записи &$scanner.

Мы также можем использовать glob-ссылки приведения в порядок подпрограммы &brack_it из предыдущего выпуска. Вместо явного разименования значения $list_ref в:


        sub brack_it {
                my($list_ref) = @_;
                foreach (@$list_ref) {
                        print "[$_]"; # печать элементов в скобках
                }
                print "\n";
        }

мы можем заменить его на glob-присвоение:


        sub brack_it {
                local(*list) = @_; # надеемся, что ссылка на список
                foreach (@list) {
                        print "[$_]"; # печать элементов в скобках
                }
                print "\n";
        }

Другим использованием glob-присвоений является возможность сделать подпрограмму сортировки более родовой (общей). Hапример, классическая ``сортировка по значению'' для конкретного хеша записывается как:


        sub by_numeric_value {
                $hash{$a} <=> $hash{$b}
        }

что работает хорошо, поскольку подпрограмма сортировки получает данные из хеша %hash, как в этом примере:


        sub sort_hash_by_value {
                sort by_numeric_value keys %hash;
        }
        @them = &sort_hash_by_value;

Здесь, значения в массиве @them являются ключами %hash сортироваными числовым значениям. Мы можем сделать данную подпрограмму более общей:


        sub sort_by_value {
                local(*hash) = @_; # ссылка на хеш
                sort by_numeric_value keys %hash;
        }
        @them_hash = &sort_by_value(\%hash);

До настоящего времени это выполняет тоже самое, что и предыдущая подпрограмма, но я передаю имя %hash в качестве первого аргумента. Затем на него создается алиас и подпрограмма работает как и прежде. Она становится лучше, поскольку я могу передать другие хеши:


        @them_there = &sort_by_value(\%there);

и что выполняет теже действия над хешем %there! В этом случае, подпрограмма сортировки &sort_hash_by_value думает, что она имеет доступ к %hash, а в действительности она имеет доступ к %there, поскольку используется алиас. Очень хорошо.

Снова, я надеюсь, что эта экскурсия по возможностям Perl (особенно по наиболее мощным свойствам ссылок) была полезна для вас. Hаслаждайтесь!


Next Previous Contents