В моей предыдущей заметке я ввел понятие ``ссылки'' в 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аслаждайтесь!