В моей предыдущей заметке я ввел понятие ``ссылки'' в 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 = ÷($op1,$op2); } print "result is $res\n";
Теперь подумайте, как это усложниться, если у меня будет 15 операторов. Одинаковость шаблонов кода заставляет меня думать, что я могу факторизовать этот код, и в действительности я могу это сделать используя ссылки.
## инициализация таблицы операторов %op_table = ( "+" => \&add, "-" => \&subtract, "*" => \&multiply, "/" => \÷, ); 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
,
÷
были скорее капризом. При добавлении операторов, я
должен был сохранять именование подпрограмм, даже хотя имена были
использованы только в одном месте -- в таблице
%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аслаждайтесь!