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

Наш баннер
Вы можете установить наш баннер на своем сайте или блоге, скопировав этот код:
RSS новости