Детский Perl доведёт вас лишь до сюда. Беглость языка позволит вам использовать естественные шаблоны и идиомы языка. Эффективные программисты понимают, как возможности Perl взаимодействуют и сочетаются друг с другом.
Приготовьтесь к второй кривой обучения Perl: Perl-мышление. Результатом будет лаконичный, мощный код в стиле Perl.
Каждый язык — язык программирования или естественный — имеет распространённые шаблоны выражений, или идиомы. Земля вращается, но мы говорим о восходящем и заходящем солнце. Мы хвастаемся умными хаками и и морщимся от страшных хаков в процессе работы с кодом.
Идиомы Perl — не совсем возможности языка или техники дизайна. Это манеры и механизмы, которые, сложенные вместе, дают вашему коду перловый акцент. Вы не обязаны их использовать, но они придают Perl силу.
$self
Объектная система Perl 5 (Moose) работает с инвокантом метода как с обычным параметром. Независимо от того, вызываете ли вы метод класса или экземпляра, первый элемент @_
— всегда инвокант метода. По соглашению, большая часть кода на Perl 5 использует $class
как имя инвоканта метода класса и $self
как имя инвоканта объекта. Это соглашение достаточно сильно, чтобы полезные расширения, такие как MooseX::Method::Signatures
, исходили из предположения, что бы используете $self
как имя объектных инвокантов.
Обработка списков — фундаментальная составляющая обработки выражений в Perl 5. Способность Perl-программистов объединять в цепочки выражения, возвращающие списки переменной длины, предоставляет бесчисленные возможности для эффективного манипулирования данными.
Хотя простота передачи аргументов в Perl 5 (всё разглаживается в @_
) иногда слишком проста, присваивание из @_
в списочном контексте позволяет вам распаковывать именованные параметры как пары. Оператор толстой запятой (Объявление хешей) превращает простой список в то, что очевидно является списком пар аргументов:
make_ice_cream_sundae(
whipped_cream => 1,
sprinkles => 1,
banana => 0,
ice_cream => 'mint chocolate chip',
);
Вызываемая сторона может распаковать эти параметры в хеш и работать с хешем, как если бы он был одним аргументом:
sub make_ice_cream_sundae
{
my %args = @_;
my $dessert = get_ice_cream( $args{ice_cream} );
...
}
Perl Best Practices предлагает вместо хешей передавать ссылки на хеши. Это позволяет Perl выполнять валидацию ссылки на хеш на вызываемой стороне.
Эта техника хорошо работает с import()
(Импорт) или другими методами; обработайте столько параметров, сколько хотите, прежде чем поглотить оставшуюся часть в хеш:
sub import
{
my ($class, %args) = @_;
my $calling_package = caller();
...
}
Преобразование Шварца — элегантная демонстрация всепроникающей обработки списков в Perl в виде идиомы, удачно позаимствованной из семейства языков Lisp.
Предположим, у вас есть Perl-хеш, ассоциирующий имена ваших коллег с их телефонными номерами:
my %extensions =
(
'001' => 'Armon',
'002' => 'Wesley',
'003' => 'Gerald',
'005' => 'Rudy',
'007' => 'Brandon',
'008' => 'Patrick',
'011' => 'Luke',
'012' => 'LaMarcus',
'017' => 'Chris',
'020' => 'Maurice',
'023' => 'Marcus',
'024' => 'Andre',
'052' => 'Greg',
'088' => 'Nic',
);
Заключение в кавычки ключа хеша с помощью толстой запятой работает только с тем, что выглядит как голые слова; эти ключи выглядят как числа — восьмеричные числа, если быть конкретным, с ведущим нулём. Да, почти все делают эту ошибку.
Чтобы отсортировать список по имени в алфавитном порядке, вы должны отсортировать хеш по его значениям, не ключам. Получить корректно отсортированные значения легко:
my @sorted_names = sort values %extensions;
…но вам нужен дополнительный шаг для сохранения связи имён и телефонов, тут и вступает в дело преобразование Шварца. Во-первых, сконвертируйте хеш в список структур данных, которые легко сортировать — в данном случае, двухэлементных анонимных массивов:
my @pairs = map { [ $_, $extensions{$_} ] }
keys %extensions;
sort
принимает список анонимных массивов и сравнивает их вторые элементы (имена) как строки:
my @sorted_pairs = sort { $a->[1] cmp $b->[1] }
@pairs;
Блок, переданный в sort
, принимает её аргументы в виде двух переменных $a
и $b
, принадлежащих области видимости пакета (footnote: См. в perldoc -f sort
пространное обсуждение применений такой организации области видимости.). Блок sort
принимает свои аргументы по два за раз; первый становится содержимым $a
, а второй — содержимым $b
. Если значение $a
должно идти в результатах перед $b
, блок должен вернуть -1. Если оба значения достаточно эквиваленты с точки зрения сортировки, блок должен вернуть 0. Наконец, если $a
должно идти в результатах после $b
, блок должен вернуть 1. Любые другие возвращаемые значения являются ошибками.
Переворачивание хеша на месте будет работать, если имена ни у кого не повторяются. Этот конкретный набор данных не имеет такой проблемы, но пишите код с защитой.
Оператор cmp
выполняет строковое сравнение, а <=>
выполняет числовое сравнение.
Имея @sorted_pairs
, вторая операция map
преобразует структуру данных в более удобную форму:
my @formatted_exts = map { "$_->[1], ext. $_->[0]" }
@sorted_pairs;
…и теперь вы можете всё это вывести:
say for @formatted_exts;
Преобразование Шварца само по себе использует всепроникающую обработку списков в Perl чтобы избавиться от временных переменных. Комбинация такая:
say for
map { " $_->[1], ext. $_->[0]" }
sort { $a->[1] cmp $b->[1] }
map { [ $_ => $extensions{$_} ] }
keys %extensions;
Читайте выражение справа налево, в порядке вычисления. Для каждого ключа в хеше телефонов создать двухэлементный анонимный массив, содержащий ключ и значение из хеша. Отсортировать этот список анонимных массивов по их вторым элементам, значениям из хеша. Сформировать строку вывода из этих отсортированных массивов.
Канал map
—sort
—map
в преобразовании Шварца трансформирует структуру данных в другую, более подходящую для сортировки, а затем трансформирует её снова в другую форму.
Хотя этот пример сортировки прост, рассмотрите случай вычисления криптографического хеша для большого файла. Преобразование Шварца особенно полезно, потому что оно эффективно кеширует любые дорогие вычисления, выполняя их один раз в правом map
.
local
имеет большое значения для управления магическими глобальными переменными Perl 5. Вы должны понимать области видимости (Область видимости), чтобы эффективно использовать local
— но если вам это удастся, вы можете использовать тесные и легковесные области видимости интересными способами. Например, для поглощения файлов в скаляр в одном выражении:
my $file = do { local $/ = <$fh> };
# или
my $file = do { local $/; <$fh> };
# или
my $file; { local $/; $file = <$fh> };
$/
— это разделитель входных записей. Его локализация с помощью local
устанавливает его значение в undef
, ожидая присваивания. Эта локализация происходит перед присваиванием. Так как значение разделителя неопределённо, Perl охотно считает всё содержимое дескриптора файла сразу и присвоит это значение $/
. Так как блок do
возвращает значение последнего вычисленного внутри блока выражения, это возвращает значение присваивания: содержимое файла. Даже несмотря на то, что в конце блока $/
немедленно возвращается к своему предыдущему состоянию, $file
теперь вмещает содержимое файла.
Второй пример не содержит присваивания и всего лишь возвращает единственную строку, прочитанную из дескриптора файла.
Третий пример избегает повторного копирования строки, вмещающей содержимое файла; он не так красив, но использует меньше памяти.
Общеизвестно, что этот полезный пример сводит с ума людей, не понимающих, как работает и local
, и области видимости. Модуль File::Slurp
из CPAN — стоящая (и зачастую более быстрая) альтернатива.
Perl не требует специального синтаксиса для создания замыканий (Замыкания); вы можете непреднамеренно замкнуть лексическую переменную. Многие программы обычно выставляют несколько лексичеких переменных области видимости файла, прежде чем передать обработку другим функциям. Возникает соблазн использовать эти переменные напрямую, вместо передачи и возврата значений из функций, особенно с ростом программы. К сожалению, эти программы могут начать полагаться на тонкости того, что происходит во время процесса компиляции Perl 5; переменная, которая, как вы думали, должна быть инициализирована определённым значением, может быть инициализирована лишь намного позже.
Чтобы этого избежать, оберните основной код вашей программы в простую функцию, main()
. Инкапсулируйте ваши переменные в соответствующих областях видимости. Затем добавьте единственную строку в начало вашей программы, после подключения всех нужных модулей и прагм:
#!/usr/bin/perl
use Modern::Perl;
...
exit main( @ARGS );
Вызов main()
перед чем либо ещё в программе заставляет вас быть ясными относительно инициализации и порядка компиляции. Вызов exit
с возвращаемым main()
значением предотвращает любой другой голый код от выполнения, хотя вы должны удостовериться, что возвращаете 0
из main()
в случае успешного выполнения.
Фактическое различие между программой и модулем — в их намеренном использовании. Пользователи вызывают программы напрямую, тогда как программы загружают модули после того, как выполнение уже началось. Тем не менее, модуль — это Perl-код, точно так же, как и программа. Модуль легко сделать исполняемым. Так же как и заставить программу вести себя как модуль (полезно для тестирования частей существующей программы без формального преобразования её в модуль). Всё что вам нужно сделать — открыть для себя, как Perl начинает выполнение куска кода.
Единственный необязательный аргумент caller
— это число фреймов вызова (Рекурсия), которое нужно выдать. caller(0)
выводит информацию о текущем фрейме вызова. Чтобы позволить модулю работать корректно как программа или модуль, поместите весь выполняемый код в функции, добавьте функцию main()
и допишите одну строку в начало модуля:
main() unless caller(0);
Если у модуля нет вызывающего кода, значит, кто-то вызвал его напрямую как программу (с помощью perl path/to/Module.pm
) вместо use Module;
).
Восьмой элемент списка, возвращаемого из caller
в списочном контексте, будет истинным значением, если фрейм вызова представляет собой use
или require
, и undef
в противном случае. Хотя это более точно, немногие этим пользуются.
В CPAN есть несколько модулей, помогающих проверить параметры ваших функций; два хороших варианта — Params::Validate
и MooseX::Params::Validate
. Выполнить простую валидацию легко даже без этих модулей.
Предположим, ваша функция принимает два аргумента, не больше и не меньше. Вы могли бы написать:
use Carp 'croak';
sub groom_monkeys
{
if (@_ != 2)
{
croak 'Grooming requires two monkeys!';
}
...
}
…но с лингвистической точки зрения последствия более важны, чем проверка, и заслуживают быть в начале выражения:
croak 'Grooming requires two monkeys!' if @_ != 2;
…что может читаться проще в следующем виде:
croak 'Grooming requires two monkeys!'
unless @_ == 2;
Эта техника раннего возврата — особенно с постфиксными условиями — может упростить оставшуюся часть кода. Каждая такая проверка — это фактически одна строка в таблице истинности.
Многие идиомы Perl 5 полагаются на тот факт, что выражения возвращают значения:
say my $ext_num = my $extension = 42;
Хотя этот код очевидно неуклюж, он демонстрирует, как использовать значение одного выражения в другом выражении. Эта идея не нова; вы с большой вероятностью и раньше использовали возвращаемое значение функции в списке или как аргумент другой функции. Вы, возможно, не осознавали последствий этого.
Предположим, вы хотите извлечь имя из сочетания имени и фамилии с помощью прекомпилированного регулярного выражения в $first_name_rx
:
my ($first_name) = $name =~ /($first_name_rx)/;
В списочном контексте успешное сопоставление регулярному выражению возвращает список всех захватов (Захват), и Perl присваивает первый из них $first_name
.
Чтобы изменить имя, возможно, удалив все несловарные символы для создания пригодного имени пользователя для системного аккаунта, вы можете написать:
(my $normalized_name = $name) =~ tr/A-Za-z//dc;
В Perl 5.14 был добавлен недеструктивный модификатор замены /r
, так что вы можете написать my $normalized_name = $name =~ tr/A-Za-z//dcr;
.
Сперва присвойте значение $name
$normalized_name
, так как скобки влияют на приоритет, это присваивание произойдёт первым. Выражение присваивания возвращает переменную $normalized_name
, так что эта переменная становится первым операндом оператора транслитерации.
Эта техника работает и с другими операциями модификации на месте:
my $age = 14;
(my $next_age = $age)++;
say "Next year I will be $next_age";
Система типов Perl 5 почти всегда делает то, что нужно, если вы выбираете правильные операторы. Используйте оператор строковой конкатенации, и Perl будет обращаться с обоими операндами как со строками. Используйте оператор сложения, и Perl будет обращаться с обоими операндами как с числовыми.
Иногда вам придётся дать Perl подсказку о том, что вы имеете ввиду, с помощью унарного приведения типа, чтобы заставить вычисление значения выполняться конкретным образом.
Чтобы убедиться, что Perl обрабатывает значение как числовое, прибавьте ноль:
my $numeric_value = 0 + $value;
Чтобы убедиться, что Perl обрабатывает значение как булево, используйте двойное отрицание:
my $boolean_value = !! $value;
Чтобы убедиться, что Perl обрабатывает значение как строку, конкатенируйте его с пустой строкой:
my $string_value = '' . $value;
Хотя потребность в этих приведениях типа исчезающе мала, вы должны понимать эти идиомы, если они вам встретятся. Хотя удаление «бесполезного» + 0
из выражения может выглядеть безопасным, это может сломать код.
Perl 5 предоставляет несколько суперглобальных переменных, которые поистине глобальны, не ограничены областью видимости пакета или файла. К сожалению, их глобальная доступность означает, что любые прямые или непрямые модификации могут повлиять на другие части программы — и они немногословны. Опытные программисты на Perl 5 запоминают некоторые из них. Мало кто помнит их все. Лишь некоторые из них бывают полезны. perldoc perlvar
содержит исчерпывающий список таких переменных.
Perl 5 продолжает перемещать ещё больше глобального поведения в лексическое поведение, так что вы можете избежать многих из этих глобальных переменных. Когда вы не можете их избежать, используйте local
в наименьшей возможной области видимости чтобы ограничить любые изменения. Вы всё ещё восприимчивы к любым изменениям, которые вызываемый вами код сделает с этими глобальными переменными, но вы уменьшаете вероятность неожиданного кода снаружи вашей области видимости. Как демонстрирует идиома лёгкого поглощения файла (Лёгкое поглощение файлов), local
зачастую правильный подход:
my $file; { local $/; $file = <$fh> };
Эффект локализации $/
длится только до конца блока. Невелик шанс, что какой-либо Perl-код будет выполнен как результат чтения строк из дескриптора файла (footnote: Связанный дескриптор файла (Связывание) — одна из немногих вероятностей.) и изменит значение $/
внутри блока do
.
Не все случаюи использования суперглобальных переменных так легко защитить, но это зачастую работает.
В других случаях вам нужно прочитать значение суперглобальной переменной, и надеяться, что другой код его не изменил. Отлавливание исключений с помощью блока eval
может быть восприимчиво к условиям гонки, поскольку метод DESTROY()
, вызванный на лексической переменной, которая вышла за пределы области видимости, может сбросить $@
:
local $@;
eval { ... };
if (my $exception = $@) { ... }
Копируйте $@
сразу же после поимки исключения, чтобы сохранить её содержимое. Смотрите также Try::Tiny
вместо этого (Предостережения об исключениях).
Базовый модуль English
предоставляет подробные имена для насыщенных пунктуацией суперглобальных переменных. Импортируйте их в пространство имён следующим образом:
use English '-no_match_vars';
Это позволит вам использовать подробные имена, документированные в perldoc perlvar
, внутри области видимости этой прагмы.
Три связанных с регулярными выражениями суперглобальных переменных ($&
, $`
и $'
) приводят к снижению производительности всех регулярных выражений в программе. Если вы забудете -no_match_vars
при импорте, ваша программа получит минус производительности, даже если вы не читаете явно из этих переменных.
Программы на Современном Perl должны использовать переменную @-
как замену для этих ужасных трёх.
Большая часть программ на современном Perl 5 может обойтись использованием только нескольких из суперглобальных переменных. Вы с большой вероятностью встретите лишь немногие из этих переменных в реальных программах.
$/
(или $INPUT_RECORD_SEPARATOR
из прагмы English
) это строка из нуля или более символов, обозначающая конец записи при построчном чтении входных данных. По умолчанию, это платформозависимая последовательность символов новой строки. Если вы сделаете это значение неопределённым, Perl будет пытаться прочитать файл в память целиком. Если вы установите это значение в ссылку на целое число, Perl будет пытаться прочитать именно столько байт на запись (так что не забывайте о вопросах Юникода). Если вы установите это значение в пустую строку (''
), Perl будет читать по параграфу за раз, где параграф — это порция текста, за которой следует произвольное количество новых строк.
$.
($INPUT_LINE_NUMBER
) содержит количество записей, прочитанных из дескриптора файла, к которому последний раз осуществлялся доступ. Вы можете читать из этой переменной, но запись в неё не даст никакого эффекта. Локализация этой переменной будет локализовать дескриптор файла, на который она ссылается.
$|
($OUTPUT_AUTOFLUSH
) управляет тем, будет ли Perl сбрасывать всё, что записано в текущий выбранный дескриптор файла, сразу же, или только когда буфер Perl заполнится. Небуферизованный вывод полезен при записи в канал, или сокет, или терминал, который не должен блокироваться ожиданием ввода. Эта переменная будет приводить любые присвоенные ей значение в булевы.
@ARGV
содержит агрументы командной строки, переданные в программу.
$!
($ERRNO
) это двойная переменная (Двойные переменные), содержащая результат последнего системного вызова. В числовом контексте она соответствует значению errno
в C, где всё, кроме ноля, обозначает ошибку. В строковом контексте она возвращает соответствующую строку системной ошибки. Локализуйте эту переменную, прежде чем выполнить системный вызов (явно или неявно), чтобы избежать перезаписывания соответствующего значения для другого кода где бы то ни было. Многие места в самом Perl 5 делают системные вызовы, о которых вы не знаете, так что значение этой переменной может измениться независимо от вас. Копируйте его сразу же после выполнения системного вызова для получения наиболее точных результатов.
$"
($LIST_SEPARATOR
) это строка, используемая для разделения элементов массивов и списков, интерполируемых в строку.
%+
содержит именованные захваты из успешного сопоставления регулярному выражению (Именованные захваты).
$@
($EVAL_ERROR
) содержит значение, выброшенное из последнего исключения (Отлавливание исключений).
$0
($PROGRAM_NAME
) содержит имя выполняемой программы. Вы можете модифицировать это значение на некоторых Unix-подобных платформах чтобы изменить имя программы, под которым она предстаёт перед другими программами, такими как ps
или top
.
$$
($PID
) содержит идентификатор процесса текущего выполняемого экземпляра программы, как его понимает операционная система. Он будет различаться между программами, разветвлёнными с помощью fork()
, и может различаться между потоками в одной и той же программе.
@INC
содержит список путей в файловой системе, по которым Perl будет искать файлы для загрузки с помощью use
или require
. Смотрите perldoc -f require
для информации о других элементах, которые этот массив может содержать.
%SIG
ставит в соответствие низкоуровневым сигналам ОС и Perl ссылки на функции, используемые для обработки этих сигналов. Например, можно отловить стандартное прерывание по Ctrl-C с помощью сигнала INT
. См. perldoc perlipc
для большей информации о сигналах и особенно безопасных сигналах.
Самые тяжёлые обвинения о воздействии на расстоянии относятся к вводу/выводу и исключительным условиям. Использование Try::Tiny
(Предостережения об исключениях) поможет вам защититься от хитрой семантики правильной обработки исключений. Локализация и копирование значения $!
может помощь вам избежать странных поведений, когда Perl выполняет неявные системные вызовы. Использование IO::File
и его методов на лексических дескрипторах файлов (Специальные переменные обработки файлов) поможет предотвратить нежелательные глобальные изменения поведения ввода/вывода.