Логотип StingRay

Поделиться
FacebookFacebookRSSTwitterYouTubeВ контактеОдноклассники
FacebookFacebookRSSTwitterYouTubeВ контактеОдноклассники
Силуэт человека

Критический анализ языка PERL

Аннотация
Введение
1. Лексемы
 1.1. Комментарии
 1.2. Идентификаторы
 1.3. Числа
 1.4. Строки
2. Типы данных, переменные и константы
 2.1. Скаляры
 2.2. Массивы
 2.3. Хеши
 2.4. Переменные и константы
3. Операции и выражения
 3.1. Арифметические операции
 3.2. Операции сравнения
 3.3. Прочие операции и особенности выражений
4. Операторы
 4.1. Операторы присваивания
 4.2. Операторы ветвления
 4.3. Операторы цикла
 4.4. Операторы безусловного перехода
5. Подпрограммы
 5.1. Синтаксис определения и вызова подпрограмм
 5.2. Передача параметров и возврат значений
 5.3. Область видимости идентификаторов
6. Модульное программирование
 6.1. Синтаксис определения модуля
 6.2. Экспорт и импорт
7. Объектно-ориентированное программирование
 7.1. Синтаксис определения класса, область видимости методов и свойств
 7.2. Вызов методов
 7.3. Создание и удаление объектов
 7.4. Наследование и полиморфизм
Заключение
Литература

Аннотация

Статья посвящена популярному языку программирования PERL. В данной статье представлен детальный анализ и критика основных языковых средств PERL. Подробно рассмотрены все элементы синтаксиса, начиная с лексем и заканчивая средствами поддержки ООП. Особое внимание уделено понятию типа данных и механизму типизации, а также базовым понятиям и концепциям, лежащим в основе модульного и объектно-ориентированного программирования.

Осознание изложенных в статье идей не предполагает знакомства с языком PERL, однако необходимо знание какого-либо языка и понимание основных понятий, лежащих в основе программирования, например, подпрограмма, модуль, класс, тип данных и проч.

Статья предназначена для всех, интересующихся программированием вообще и языком PERL в частности. Автор надеется, что данная статья будет интересна и полезна как начинающим, так и опытным программистам.

Введение

Данная статья посвящена критике языка PERL и является выражением субъективных взглядов автора на данный язык, сформировавшихся в результате его практического применения в сфере разработки Internet-приложений для e-commerce. Предметом данной статьи является, прежде всего, сам язык PERL, то есть, главным образом, его конструкции ("языковые средства"), а также особенности их использования. При этом встроенные переменные и функции рассматриваются лишь отчасти и только в контексте тех или иных конструкций. Приёмы или стиль программирования на языке PERL, равно как и возможности его использования для решения тех или иных конкретных классов задач не рассматриваются вовсе и не являются предметом настоящей статьи. Статья построена по схеме "от простого – к сложному", то есть последовательно рассматриваются все основные элементы языка, начиная от лексем и заканчивая средствами поддержки ООП.

Web-программированием сегодня не занимается разве что ленивый, так что, предвидя нападки со стороны "корифеев" от Apache+CGI типа "а зачем PERL – ведь есть php!", автор желает заявить следующее. Здесь язык PERL намеренно рассматривается в отрыве от тех областей, в которых он получил наиболее широкое применение (автоматизация административных задач в системах типа Unix и Web-программирование). Это обусловлено сильным развитием как самого языка, так и его "окружения" (обилие разного рода библиотек, модулей, классов, а также вспомогательных инструментов), что позволяет рассматривать PERL как, фактически, универсальную среду программирования, практически пригодную для решения очень разных задач, включая не только серверные приложения, но и приложения, ориентированные на конечного пользователя (средства поддержки столь модных нынче графических интерфейсов также имеются).

И ещё. Автор вовсе не считает себя "гуру" от программирования, однако вполне определённый опыт, полученный при выполнении конкретных проектов, а также разносторонние познания в этой области (заметим лишь, что количество языков программирования, которые освоены автором на практике, переваливает за десяток) дают автору основание и моральное право высказывать свои критические замечания, которые отнюдь не являются следствием эмоций или поверхностных представлений, а суть обоснованные суждения, в чём читателю и предлагается убедиться. Везде, где это уместно, автор проводит аналогии с некоторыми из известных ему языков программирования.

Автору хотелось бы сделать ещё одно замечание касательно терминологии. По сути, все известные автору реализации языка PERL, включая стандартную, поставляемую в составе Unix-систем, являются интерпретаторами. Однако очевидно, что PERL не является строго построчным интерпретатором, поскольку он выполняет синтаксический анализ всей программы, включая внешние модули, загруженные с помощью предложения use, ещё до начала её выполнения, при этом, по-видимому, создаётся некоторое внутреннее представление по крайней мере какой-то части программы. Правда, здесь есть некоторые исключения, вроде блоков BEGIN, которые выполняются сразу же после их анализа, даже если анализ всей программы ещё не завершён, или модулей, подключаемых с помощью предложения require, анализ которых выполняется только после выполнения самого предложения require, но эти исключения, по большому счёту, не изменяют положения дел (тем более что, скажем, предложение require использовать не рекомендуется), поскольку запуск программы на выполнение может быть чётко разделён на две фазы: синтаксический анализ и собственно выполнение (интерпретация). В литературе по PERL почему-то принято фазу синтаксического анализа называть этапом компиляции, что, вообще говоря, не совсем верно, однако автор не стал ломать устоявшуюся традицию, так что при обсуждении вопросов, относящихся к фазе синтаксического анализа, автор будет применять термин "этап компиляции".

Соответственно, при обсуждении вопросов, относящихся к моменту собственно выполнения программы, автор будет употреблять термин "этап выполнения". Если необходимо особо подчеркнуть, что те или иные результаты могут быть получены только на этапе выполнения, и причём, возможно, для получения этих результатов может потребоваться провести специально спланированные испытания, то автор будет употреблять словосочетание "на этапе выполнения при отладке" (некоторые почему-то считают, что отладка – это приведение программы к "компилируемому состоянию", что является величайшим заблуждением вообще, а в случае PERL, как будет показано в дальнейшем, особенно).

В условиях отсутствия формального определения языка PERL, при написании статьи автор использовал, помимо собственного опыта, документацию, которая входит в "комплект поставки" PERL (главным образом man-страницы), как наиболее авторитетный источник. Кроме того, полезной оказалась книга [1], написанная Larry Wall (автором данного языка), под названием "Programming PERL", вышедшая в издательстве O'Reilly & Associates, которая позволила познакомиться со взглядами автора языка PERL на жизнь вообще и на программирование в частности. При изложении тех или иных фактов, почерпнутых из документации, автор старался давать ссылки на конкретные man-страницы. Все цитаты, приведённые в данной статье, снабжены ссылками на соответствующие источники, список которых приведён к конце статьи. Цитаты из англоязычных источников снабжены переводами.

Всё, о чём говорится в данной статье, относится к языку PERL версии 5.005. Все упомянутые в статье факты, относящиеся к языку PERL, и все приведённые в тексте примеры проверены как на платформе Unix (Red Hat Linux 6.02), так и на платформе Win32 (Microsoft Windows 95 OSR 2). Все примеры снабжены комментариями, поясняющими ту или иную мысль, которую эти примеры призваны проиллюстрировать. Факты, относящиеся к другим языкам программирования, также проверены автором лично.

1. Лексемы

Анализ языка PERL начнём с исследования его лексем. В данном разделе рассмотрены не все классы лексем языка PERL, а только те, чьи особенности реализации представляют интерес с точки зрения критики.

1.1. Комментарии

В PERL предусмотрено два типа комментариев: однострочный и документирующий. Многострочный комментарий отсутствует, а ведь "комментарии в общем будут лучшими, когда они помещаются в многострочных блоках, которые чередуются с блоками кода программы. Для этого комментарий должен на высоком уровне описывать, что делают несколько последующих строк кода. Если комментарии попадаются через строчку, то это похоже на чтение двух книг одновременно, причём по строке из каждой поочерёдно" [7]. Не случайно, наверное, в языках Pascal и C многострочный комментарий является единственным поддерживаемым на уровне языка средством документирования программ.

В качестве многострочного комментария в PERL предлагается использовать документирующий, однако это не всегда удобно, поскольку любой таким образом оформленный многострочный комментарий будет включён в выходной файл, генерируемый утилитой perldoc, как часть документации модуля. Кроме того, pod-директивы, используемые при оформлении документирующего комментария, должны начинаться в первой позиции строки (символом "="), что также не особенно удобно при обычном многострочном комментировании.

Однако можно применить один трюк, который позволяет включать многострочные комментарии так, чтобы они не попали в выходную документацию. Он состоит в том, чтобы вместо стандартных pod-директив использовать незнакомую perldoc директиву, в результате данная директива не будет интерпретироваться PERL, поскольку в первой позиции строки стоит символ "=", но и perldoc она будет молча пропущена. Кстати, этот трюк описан в man-страницах (именно perlsyn), поставляемых с PERL.

Кстати, поддержка документирующего комментария как отдельного синтаксического средства представляется избыточным – при должной реализации соответствующей утилиты выделения документации из исходного модуля (в случае PERL это perldoc) можно было бы обойтись многострочным комментарием с включением в него специальных тегов, распознаваемых этой утилитой.

У любого здорового программиста в связи с этим возникает один вопрос: а что мешало сделать так, как сделано, скажем, в Java? Это вопрос чисто риторический – трюкачество без меры есть стиль программирования, навязываемый языком PERL.

1.2. Идентификаторы

Правила построения идентификаторов в PERL, мягко говоря, причудливы. Кроме стандартного набора букв, цифр и символа "_" в пользовательских идентификаторах допускается использование символа "'", причём в качестве первого тоже. Зачем и кому это понадобилось – непонятно, ведь данная особенность приводит к появлению ещё одного (среди бесчисленного множества других) потенциального источника ошибок, которые, правда, можно обнаружить на этапе компиляции. Так, следующий код компилируется исправно:

$'Foo = "john";
print($'Foo);

А этот приведёт к ошибке компиляции:

$Foo' = "john";
print($Foo');

На это можно возразить, что, мол, если не нужно – не используйте. Однако здесь есть ещё одна тонкость, которая связана с модульностью. Дело в том, что в ранних версиях PERL в качестве разделителя элементов квалифицированного идентификатора использовался именно символ "'", причём в целях обратной совместимости это поддерживается до сих пор. А это может привести к ошибке, которую будет очень трудно найти. Например, следующий код будет компилироваться и выполняться без ошибок, но результат, скорее всего, будет отличным от ожидаемого:

$Owner = "John";
print("That's $Owner's house.");

Если только у вас на самом деле нет модуля Owner, который экспортирует переменную s, то на терминале будет напечатано:

That's  house.

Эта тонкость (чтобы не назвать глупостью), к счастью, описана в man-страницах (именно perlmod), откуда и позаимствован приведённый пример. Теперь, кстати, в качестве символа-разделителя в квалифицированных идентификаторах используется составной символ "::". Наверное, ребята ну очень хотели сделать всё как в C++ (о чём прямо заявлено в той же perlmod). Но что же предлагается использовать для решения этой проблемы? А вот что:

print("That's ${Owner}'s house."); # блок или элемент хеша?

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

1.3. Числа

И здесь не обошлось без сюрпризов. Кроме стандартных форм записи целых и вещественных чисел вроде

123     # целое
123.123 # вещественное
.5      # при записи десятичных дробей ноль в целой части можно опускать
.5E-10  # экспоненциальная форма
0xABCD  # шестнадцатеричные числа записываются как в C
0377    # если первая цифра - ноль, то это восьмеричное число

PERL поддерживает и такую:

9_123_456 # это, видимо, для "удобства чтения"

и даже, как показывает практика, такую:

9_1__2_3___456_____ # что сие означает, надо спросить у Larry Wall

Но только будьте осторожны! Ни в коем случае не ставьте символ "_" в начале числа – будут сюрпризы, например:

$f = _1;
print($f + 2); # будет напечатано 2 вместо ожидаемых 3
print(_1 + 2); # а так вообще ничего не будет напечатано, здорово, правда?

PERL скорее сделает невозможное, чем выдаст сообщение об ошибке. Ваша программа заработает сразу же и будет работать всегда, не вызывая сообщений об ошибках, только насколько правильно – это, как говорится, "тонкий философский вопрос".

1.4. Строки

Строки обычно считаются обычными лексемами, но в PERL – это операции. Дело в том, что существует несколько способов обозначения строк, в зависимости от используемого способа PERL выполняет те или иные преобразования над их содержимым на этапе выполнения. Причём для каждого способа существует две формы: сокращённая и полная. При использовании полной формы программисту предоставлена возможность самостоятельно выбрать символы, которые будут использованы как символы-ограничители строки, причём если в качестве символов-ограничителей выбраны скобки (круглые, прямоугольные, угловые или фигурные), то они обязательно должны быть парными, в остальных случаях в качестве начального и концевого ограничителя должен использоваться один и тот же символ. В приведённой ниже таблице (она позаимствована из man-страницы perlop) указаны обе формы для каждого из способов обозначения строк и их назначение, при описании полной формы для определённости в качестве символов-ограничителей использованы фигурные скобки.

Сокращённая форма Полная форма Выполнение подстановок Назначение
'' q{} нет строковый литерал
"" qq{} да строковый литерал
`` qx{} да, если символ-ограничитель не "'" команда системного интерпретатора команд
qw{} нет массив строк, содержащих слова исходной строки
// m{} да сопоставление с шаблоном
qr{} да предварительная компиляция шаблона
s{}{} да замена подстроки по шаблону
tr{}{} нет замена/удаление символов

Если использованный способ обозначения строки допускает выполнение подстановки, то перед преобразованием PERL, рассматривая подстроки, следующие после символов "$" и "@", встречающихся в строке, как идентификаторы соответствующих скаляров и массивов, вставляет их значения, которые автоматически преобразуются к строковому типу, в строку, как если бы они были указаны непосредственно, причём значения элементов массива сливаются в одну сплошную подстроку. Кроме того, распознаются около 10 esc-последовательностей, начинающихся с символа "\" ("\n", "\t" и проч.).

Автоматические подстановки значений переменных в строки, а также разнообразные возможности сопоставления, выделения и замены подстрок по шаблону традиционно считаются сильной стороной языка PERL и одним из главных аргументов, используемых апологетами данного языка. Тем не менее, при ближайшем рассмотрении оказывается, что наличие данных возможностей скорее служит признаком слабости, нежели силы. Необходимость в автоматических подстановках, которые были взяты на вооружение, по всей видимости, у Bourne Shell, более чем сомнительна и, по сути, является опасной, учитывая трудности с идентификаторами, обсуждавшимися ранее. Гораздо надёжнее и проще с точки зрения чтения программ использовать операцию конкатенации строк, а не пытаться искать приключения.

Шаблоны (регулярные выражения) заслуживают отдельного обсуждения. Однако наличие встроенной непосредственно в язык поддержки такого рода возможностей, как сопоставление с шаблоном, является признаком проблемной ориентированности языка, что неприемлемо для языков общего назначения. В этом наиболее сильно проявляется тот факт, что изначально область применения языка PERL была вполне определённой и далеко не такой широкой, какой она является сейчас. Да, шаблоны полезны и позволяют решать достаточно широкий класс задач, связанных с анализом и обработкой строк, но включение их поддержки непосредственно в язык заметно усложняет его, для языка общего назначения логично было бы вынести поддержку шаблонов во внешние библиотеки. Однако в случае PERL это было бы неприемлемо с точки зрения производительности, так как, если исключить шаблоны, в нём не останется средств эффективной реализации операций обработки строк, поскольку PERL, являясь языком, реализующим концепцию автоматической сборки мусора, не поддерживает указателей и адресной арифметики в явном виде, как это делает, скажем, язык C.

Кроме того, для решения подавляющего большинства рядовых задач обработки строк, возникающих при разработке приложений, не связанных непосредственно с синтаксическим анализом, вполне достаточно простых функций, наподобие тех, что реализованы в стандартной библиотеке string языка C, тем более что их использование в подобных случаях гораздо более эффективно. Однако многочисленные руководства типа "секреты и советы" по программированию на PERL, несмотря на поддержку достаточного набора простых функций обработки строк в нём, пропагандируют использование выражений с шаблонами везде, где только возможно, даже там, где их применение ничем не оправдано и только затрудняет понимание текста программы. Так, например, для выделения TLD (top level domain) из полного доменного имени вполне можно использовать простые функции обработки строк rindex и substr, как показано в следующем фрагменте исходного кода:

$TLD = "";
$p = rindex($DomainName, ".");

# Если символ "." не найден, то доменное имя не верно,
# и TLD не может быть выделен, в противном случае выделяем TLD функцией substr
if ($p != -1) {
   $TLD = substr($DomainName, $p + 1);
}

Однако в мире программистов на PERL такой простой, ясный и эффективный подход, по-видимому, не приветствуется, поскольку нужно слишком много писать и слишком много думать. Ещё бы! Ведь если верить словам автора PERL, то "… the three great virtues of a programmer: laziness, impatience, and hubris"1 [1]. С их точки зрения, гораздо удобнее написать всё, по возможности, в одну строку, например так:

($TLD) = $DomainName =~ m/.*\.(.*)/; # $TLD необходимо заключить в круглые
                                     # скобки, указав тем списковый контекст

Однако несмотря на то, что в первом варианте в пять раз больше строк исходного кода, чем во втором, последний менее эффективен, что легко установить, выполнив приведённые примеры в цикле с контролем времени выполнения при помощи функции time. Автор неоднократно повторил данный эксперимент на компьютере с поцессором Intel Pentium 100 MHz и 32 Mb оперативной памяти, причём в одной версии шаблон, по которому велось выделение во втором фрагменте, был предварительно откомпилирован вне тела цикла при помощи qr, а в другой – этот шаблон был указан непосредственно в выражении вместе с опцией o (compile only once). Длина строки $DomainName составляла 78000 символов. Первый фрагмент в цикле из 10000 итераций потребовал для выполнения всего 1-2 секунды, а второй (в обеих версиях) – 167-168 секунд. Таким образом, пропаганда повсеместного использования операций с шаблонами фактически стимулирует применение их не по назначению, в то время как можно было бы обойтись более простыми и более эффективными средствами.

Наконец, многочисленные выражения с шаблонами могут весьма затруднить понимание программы, что становится особенно критичным при отладке или доказательстве правильности программ. В связи с этим уместно привести слова Н. Вирта: "Богатый по своим возможностям язык программирования может приветствоваться профессиональным "программистом-писателем", получающим удовольствие от процесса освоения всех его сложных особенностей. Однако интересы "программиста-читателя" требуют от языка программирования разумной скромности в предоставляемых им возможностях. Надо сказать, что к категории "программистов-читателей" могут быть отнесены не только люди (в том числе и сам автор программы), но и трансляторы с различных языков программирования и средства автоматического доказательства правильности программ" [5].

2. Типы данных, переменные и константы

Язык PERL является нетипизированным языком. В нём начисто отсутствуют базовые типы и механизм статической типизации. Это означает, что в PERL не существует таких понятий, как числовой тип, символьный тип, логический тип, строковый тип, как не существует и возможности обеспечить контроль типов при вычислении выражений и присваиваниях ни на этапе компиляции, ни на этапе выполнения. Значения любых, по сути разных, типов при необходимости преобразуются PERL автоматически на этапе выполнения, так что PERL, с точки зрения программиста, совершенно не в состоянии, например, отличить числовое значение от строкового или указателя, а строковое значение – от логического. Автоматические преобразования типов PERL выполняет исходя из контекста, в котором используется та или иная переменная. Контекст определяется, во-первых, операцией, которая должна быть выполнена со значением переменной, во-вторых, оператором, содержащим выражение, в вычислении которого участвует значение переменной, а в-третьих, также специальным символом, указанным в качестве первого символа идентификатора переменной (будем называть его символом контекста, хотя одного этого символа недостаточно для определения результата преобразования типов). Символ контекста является, фактически, неотъемлемой частью идентификатора и указывает тип структуры переменной. Существует 3 типа структур и соответствующих им символов контекста: скаляр (символ "$"), массив (символ "@") и хеш (символ "%").

Поскольку символ контекста является неотъемлемой частью идентификатора переменной, то переменные, скажем, $a, @a и %a – это совершенно разные переменные, каждой их которых соответствует своя область памяти. Однако, если вы забыли указать символ контекста перед идентификатором переменной, то, если эта переменная используется в выражении, не получите от PERL сообщения об ошибке ни на этапе компиляции, ни на этапе выполнения, а ваша программа, скорее всего, будет работать неправильно, например:

$i = 1;
$s = "Number ".i;
print($s); # печать строки "Number i" вместо ожидаемой "Number 1"

Поиск и исправление такой ситуации может потребовать очень и очень больших усилий, так что за контекстом приходится постоянно следить самому. Правда, при попытке выполнить присваивание переменной без символа контекста будет выдана ошибка компиляции, например:

$i = 1;
print(i++); # ошибка компиляции,
            # операция инкремента неявно изменяет значение переменной

И вообще, в виду отсутствия статических типов в языке PERL, у программиста нет возможности явно описывать типы и чётко отделять описания типов от описаний переменных, а описания переменных – от операций над переменными. А ведь "средства описания типов способствуют не только улучшению прозрачности и повышению надёжности программ, но и генерации… более эффективного объектного кода" [5].

2.1. Скаляры

Ввиду того, что PERL является нетипизированным языком, скаляр может принимать значения различных типов. Фактически, в качестве значения скаляра может выступать число (целое либо вещественное) либо строка, либо ссылка (адрес) на другую переменную либо функцию. Важным их свойством является то, что скаляры атомарны, то есть содержат одно и только одно значение и не имеют внутренней структуры.

В качестве значения скаляра может выступать и специальное значение undef, аналог null в SQL. В отличие от SQL, где null = null есть false, в PERL undef == undef есть true. В связи с этим непонятно, зачем ещё нужна встроенная функция defined, которая возвращает true, если её аргумент не равен undef, и false в противном случае.

В PERL отсутствуют символьный и логический типы. Если с отсутствием первого ещё можно смириться, то отсутствие второго не вызывает ничего, кроме раздражения. Исходя из руководств, в качестве логического значения true принимается любая непустая строка либо число, отличное от нуля. При этом строка "0", которая в числовом контексте эквивалентна числу 0, считается логическим false, хотя пустой не является. Более того, строка типа "000", содержащая более одного символа "0", которая в числовом контексте также эквивалентна числу 0, уже является логическим true. Но и это ещё не всё. Число 0, записанное в экспоненциальной форме как 0E0, хоть и является стопроцентным числом 0, в логическом контексте считается true. Кроме того, как оказалось, в качестве логического true воспринимается ссылка, не равная undef. И вообще, значение undef всегда считается логическим false. Таким образом, в плане поддержки логического типа язык PERL много более извращён, чем язык C. Непонятно, Larry Wall сэкономить что ли на этом решил?

Но и это ещё куда ни шло по сравнению с тем обстоятельством, что в PERL вообще невозможно различать строки и числа. Дело в том, что PERL всегда представляет числа как строки, а неявные преобразования происходят по мере необходимости при вычислениях выражений и присваиваниях. Возможно, что с точки зрения автора данного языка это "круто", да вот с точки зрения практического программирования это совсем не круто, поскольку чревато такими трудноуловимыми "глюками" (иначе и не назовёшь), о каких Larry Wall, наверное, и не догадывался. Вот реальный пример из собственной практики автора.

Итак, есть модуль, реализующий набор функций для обработки дат. Функции этого модуля, манипулирующие с датами, принимают значения дат в виде упорядоченных троек чисел, обозначающих, соответственно, год, месяц и день. Кроме того, модуль реализует пару функций, одна из которых для переданного строкового представления даты в виде "YYYY-MM-DD" (а если более точно, то в виде ^\d{1, 4}-\d{1, 2}-\d{1, 2}$; такая форма представления дат используется, например, сервером БД MySQL) возвращает соответствующую упорядоченную тройку чисел, которую затем можно использовать с функциями обработки дат, а вторая выполняет обратное преобразование, формируя из переданной упорядоченной тройки чисел строковое представление даты точно в виде "YYYY-MM-DD", которое затем можно использовать, например, при конструировании SQL-запроса. Вот их текст:

 0: sub ToString {
 1:    my ($Year, $Month, $Day) = @_;
 2:
 3:    return
 4:       (
 5:          $Year < 10 ? "000" : (
 6:             $Year < 100 ? "00" : ($Year < 1000 ? "0" : "")
 7:          )
 8:       ).$Year."-".
 9:       ($Month < 10 ? "0" : "").$Month."-".
10:       ($Day < 10 ? "0" : "").$Day;
11: }
12:
13: sub ToNumberList {
14:    my $Date = $_[0];
15:    my ($Year, $Month, $Day) =
16:       $Date =~ /^(\d{1, 4})-(\d{1, 2})-(\d{1, 2})$/;
17:
18:    if (!(defined($Year) && defined($Month) && defined($Day))) {
19:       ($Year, $Month, $Day) = (0, 0, 0);
20:    }
21:
22:    return ($Year, $Month, $Day);
23: }

Проблема была в том, что при попытке выполнить вот такой вызов с совершенно правильным аргументом

ToString(ToNumberList("0800-01-01"));

возвращалась неправильная строка "00800-001-001", которая потом приводила к ошибке там, где она использовалась далее. Причина на самом деле в том, что PERL хранит числа в виде строк, а при вычислениях автоматически преобразует строки в числа и наоборот. Поэтому для того, чтобы решить данную проблему и сделать модуль "пуленепробиваемым", пришлось строку 1 заменить на следующие:

my $Year = $_[0] + 0;
my $Month = $_[1] + 0;
my $Day = $_[2] + 0;

а строку 22 на вот это:

return ($Year + 0, $Month + 0, $Day + 0);

что хоть и выглядит очень глупо, зато работает справно.

2.2. Массивы

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

@Array = (1, 2, 3);
@NewArray = (@Array, 4, @Array); # это эквивалентно такому определению:
                                 # @NewArray = (1, 2, 3, 4, 1, 2, 3);

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

@LoL = (
   ["fred", "barney"],
   ["george", "jane", "elroy"],
   ["homer", "marge", "bart"]
);

и при этом иметь в виду контекст при получении доступа к элементам такой структуры:

print(${LoL[1]}->[1]); # печать строки "jane"
print($LoL[1][1]);     # как это ни странно, так тоже работает

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

Массивы индексируются числами (или строками, представляющими числа), начиная с нуля, то есть первый элемент имеет индекс 0, второй – 1 и т. д. Очень интересным является то, что индексы могут быть, во-первых, отрицательными, а во-вторых – даже вещественными (это же PERL!). В случае отрицательных индексов отсчёт ведётся с конца массива, то есть последний элемент имеет индекс -1, предпоследний – -2 и т. д. При индексировании вещественными числами (как положительными, так и отрицательными) в качестве индекса используется целая часть числа. Кстати, по какой-то непонятной причине возникает ошибка времени выполнения при индексации массивов вещественными числами, представленными в экспоненциальной форме (с чего бы это, ведь это же так естественно!). Но и здесь можно исхитриться, заключив записанное в экпоненциальной форме число в кавычки, вот так:

@Array = ("x", "y", "z");
print($Array["0E0"]); # печать первого элемента массива @Array

Чудеса, да и только.

В PERL отсутствуют статические массивы – все массивы являются динамически переопределяемыми. Это, с одной стороны, опасно, а с другой – неэффективно с точки зрения времени выполнения. Опасность заключается в том, что PERL не в состоянии надёжно контролировать выход индекса за пределы диапазона даже на этапе выполнения, поскольку этот диапазон как таковой отсутствует и может неявно изменяться в ходе выполнения программы (например, при присваиваниях в списковом контексте). Исключение всего одно, оно заключается в том, что при индексации отрицательными числами нельзя выполнять присваивания элементам, лежищм "левее" первого элемента (уж и не знаю, как по-другому это выразить). Таким образом, массивы не могут расти "влево", по мере уменьшения индексов, а только "вправо", по мере увеличения индексов. Все остальное разрешено, например, считывание из несуществующих элементов массива (в том числе и тех, которые "левее"), запись в сколь угодно удалённые "вправо" элементы и т. д. При необходимости, размеры массива массива увеличлежащимя, неинициализированные и несуществующие элементы при обращении к ним возвращают значение undef:

@Array = ("x", "y", "z");
$Array[5] = "a"; # теперь @Array содержит следующее:
                 # ("x", "y", "z", undef, undef, "a")

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

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

@Array = (2, 3, 4);
$s1 = @Array;    # значение $s1 равно 3
$s2 = (2, 3, 4); # значение $s2 равно 4

Таким образом, программист практически лишён автоматического контроля типов, причём не только при присваиваниях, но и при вычислении выражений, и при передаче параметров в функции. А что мешало определить стандартную функцию, которая бы возвращала текущий размер или границы диапазонов индексов массива, как это сделано, например, в языках Java Script (функция length) и Visual Basic (функции LBound и UBound)? Кстати, в PERL есть одно средство (в общепринятом лексиконе отсутствует подходящий для обозначения данного средства термин), которое позволяет узнать текущую верхнюю границу (индекс) массива, и, таким образом, его текущий размер, а также изменить его:

@Array = (2, 3, 4);
$s = $#Array; # значение $s равно 2, первый символ "#" - это не комментарий!
$#Array = 4;  # увеличиваем размер массива до 5 элементов,
              # неинициализированные элементы получают значение undef

# При выполнении следующего цикла foreach будет напечатано:
# 2
# 3
# 4
# undefined
# undefined

foreach $Element (@Array) {
   print((defined($Element) ? $Element : "undefined")."\n");
}

$#Array = 1;   # уменьшаем размер массива до 2 элементов,
               # "лишние" элементы безвозвратно уничтожаются
print(@Array); # печать строки "23"

2.3. Хеши

Хеши в PERL – это динамические структуры данных, элементы которых адресуются произвольными строками символов. Хеши были введены в язык с целью обеспечения поддержки структурного типа, недостаток которого, как было замечено в одном из руководств, так остро ощущался в ранних версиях PERL. Однако можно заметить, что хеши вовсе не являются адекватной заменой структуре (или записи, если говорить в терминах Pascal) сразу по нескольким причинам.

Во-первых, хеши имеют динамическую природу, в отличие от статических структур. Это означает, что новые ключи (и связанные с ними значения) могут быть добавлены или удалены в любое время, при этом, естественно, выполняются медленные операции с динамической памятью. Кроме того, любое обращение к элементу хеша приводит, фактически, к необходимости поиска (пусть и очень быстрого) соответствующего ключа, которые, кстати, тоже необходимо хранить, на что расходуется дополнительная память. При работе со статическими структурами вся работа по распределению памяти выполняется ещё на этапе компиляции, при этом динамическая память не используется вовсе, а все обращения к элементам структуры выполняются по статическим адресам, вычисленным на этапе компиляции, что полностью исключает потери производительности и какие бы то ни было накладные расходы по памяти. Хранить же имена полей структуры вообще нет никакой необходимости. Согласно документации (man-страница perltoot), хеши ещё менее эффективны, чем массивы, в среднем на 10-15%.

Во-вторых, вынужденное использование хешей (ввиду отсутствия структурного типа) создаёт ещё один источник потенциальных ошибок, которые могут быть обнаружены только на этапе выполнения при отладке. Дело в том, что обращение к несуществующему ключу не вызовет ошибки или предупреждения ни на этапе компиляции, поскольку это невозможно в принципе, ни на этапе выполнения – в результате будет просто возвращено значение undef. А присваивание несуществующему ключу какого-либо значения приведёт к созданию соответствующего ключа. Таким образом, необходимо самостоятельно и очень внимательно следить за всеми обращениями к ключам хеша, любая опечатка в имени ключа чревата трудноуловимыми ошибками. Для определения факта существования ключа с заданным именем в хеше используется встроенная функция exists, которая возвращает true, если заданный ключ в указанном хеше существует (даже если он имеет значение undef и функция defined для него возвращает true), и false в противном случае.

Как и в случае с массивами, скаляры можно присваивать хешам, а хеши – скалярам. В первом случае будет создан хеш с одним ключом, имя которого равно значению скаляра, и с этим ключом будет связано значение undef. Во втором случае значением скаляра будет строка вида m/n, где n – это число выделенных ячеек памяти, а m – число использованных ячеек, что различными руководствами рекомендуется использовать в качестве признака существования в хеше хотя бы одной пары ключ-значение. Такого рода присваивания абсолютно законны и не вызывают ошибок ни на этапе компиляции, ни на этапе выполнения, поскольку все преобразования типов PERL производит автоматически, так что все смысловые ошибки, связанные с неправильным использованием контекста, программист вынужден искать сам. Приведённые ниже примеры иллюстрируют только что описанные ситуации:

$s = "foo";
%h = $s;

foreach $Key (keys(%h)) {
   print($Key." => ".$h{$Key}."\n"); # будет напечатано "foo => "
}

$s = %h;

if ($s && !defined($h{"foo"})) {     # условие сработает,
   print($s);                        # и будет напечатано "1/8"
}

Кстати, присваивания хешам разрешены и для массивов. В этом случае элементы массива рассматриваются как последовательные упорядоченные пары имени ключа и соответствующего ему значения. В случае, если количество элементов массива нечётно, то с последним по порядку ключом будет связано значение undef. Присваивание же хеша массиву приводит к обратному результату, то есть элементы массива-приёмника будут содержать последовательные упорядоченные пары ключ-значение. Эта ситуация иллюстрируется следующим примером:

@a1 = ("foo1", 1, "foo2", 2);
%h = @a1;

# При выполнении следующего цикла foreach будет напечатано:
# foo1 => 1
# foo2 => 2

foreach $Key (keys(%h)) {
   print($Key." => ".$h{$Key}."\n");
}

@a2 = %h;

# При выполнении следующего цикла foreach будет напечатано:
# foo1
# 1
# foo2
# 2

foreach $Element (@a2) {
   print($Element."\n");
}

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

2.4. Переменные и константы

Объявление переменных может быть сделано в любом месте, где может стоять оператор, то есть практически везде, даже в выражении инициализации цикла for, в точности такая же ситуация наблюдается в C/C++ и Java. Но если в указанных языках обращение к или присваивание необъявленной переменной вызывает ошибку компиляции, то в PERL ни то, ни другое не вызывает никаких ошибок ни на этапе компиляции, ни на этапе выполнения, что, как и в случае с хешами, является источником потенциальных ошибок, обнаруживаемых только на этапе выполнения при отладке. Чтение несуществующей переменной возвращает значение undef, а присваивание приводит к созданию соответствующей переменной.

В PERL, так же как и в большинстве современных языков программирования, например C++, Visual Basic, Java, отсутствуют нормальные средства определения констант. Под нормальными средствами автор понимает такие, которые, помимо очевидного связывания некоторого значения или, возможно, выражения, вычислимого на этапе компиляции, с символическим именем, обеспечивают следующие возможности:

  1. ясный синтаксис определения;
  2. возможность определения констант различных типов с автоматическим контролем типа на этапе компиляции;
  3. возможность использования констант в режиме отладки;
  4. отсутствие накладных расходов по памяти на этапе выполнения.

Из всех известных автору языков программирования только язык Pascal, созданный в конце далёких 1960-x, а также "выросшие" из него языки Modula и, позднее, Oberon в полной мере обеспечивают нормальные средства определения констант, отсутствие же оных в широко разрекламированных современных "промышленных" языках программирования (это камешек в огород под названием "Java") ничего, кроме раздражения, не вызывает. Например, языки C и C++ предлагают аж три различных средства определения констант (посредством директивы препроцессора #define, модификатора const и конструктора enum), но, к великому сожалению их создателей, все они неадекватны (средства, а не создатели). Средства же, предлагаемые языком PERL, просто в высшей степени уродливы и извращены. А теперь автору хотелось бы немного перевести дух, обсуждение констант продолжим в следующем абзаце.

Итак, пример определения константы, которая трактуется как неизменяемый скаляр, в языке PERL выглядит так:

*e = \2.718282;

Вот пример синтаксиса, который ясным ну никак не назовёшь! Если константа – это скаляр, как это сказано в man-странице perlmod, то зачем здесь символ "*"? Тем более что последующие обращения к константе в данном случае следует записывать как $e, а не как *e! А что это за "\" такая перед значением константы указана? Это, между прочим, операция взятия адреса! Ну причём тут она здесь, это же значение константы, какой тут может быть адрес? А ведь её обязательно нужно указывать, в противном же случае последующие обращения к $e будут возвращать undef, а PERL никаких сообщений по этому поводу не выдаст!

Константы, вследствие того, что это, фактически, скалярные переменные, могут быть любого типа (числовые, строковые, ссылочные), но, опять же вследствие того, что это скаляры, никакого контроля типов не выполняется, при необходимости значения констант преобразуются к нужному типу автоматически исходя из контекста со всеми вытекающими отсюда плачевными последствиями. Использование констант в режиме отладки возможно, ввиду того, что это, фактически, обычные переменные, хранимые в памяти, но, опять же ввиду того, что их значения хранятся в памяти в течение всего времени выполнения программы, использование констант приводит к ничем не оправданным расходам памяти. Чтобы облегчить сопровождение, программист просто вынужден смириться с этими накладными расходами. Как говорится, из двух зол выбирают меньшее.

Справедливости ради нужно отметить, что созданные таким образом "константообразные" переменные действительно неизменяемы – при попытке присвоить значение такой переменной произойдёт ошибка этапа выполнения. Однако на этапе компиляции они не вызывают никаких сообщений. А компилятор Pascal (просто для сравнения) о такого рода ошибках выдаёт сообщение ещё на этапе компиляции.

В заключение этого раздела уместно процитировать слова Н. Вирта: "Абстракция, о которой мы не устаём говорить, – это важнейшее понятие типа данных, и мы указываем, что его ценность основана на том, что компилятор будет проверять, соблюдаются ли правила, управляющие типами, и именно компилятор будет гарантировать целостность абстракции. Если система не в состоянии это обеспечить, если она допускает выполнение логической операции над числами или не способна идентифицировать доступ к массиву с некорректным индексом – просто привожу два примера для иллюстрации – то она вряд ли может претендовать на титул "система с языком высокого уровня"" [11].

3. Операции и выражения

Данный раздел автору хотелось бы начать с цитаты: "В интересах достижения простоты и обеспечения возможности эффективной трансляции в языке Pascal существует очень маленькое число уровней приоритетов операций. В сравнении с языком Pascal язык Algol 60, в котором имеется девять уровней приоритетов операций, кажется слишком причудливым" [5]. А если более точно, то в языке Pascal определено всего четыре уровня приоритетов. В языках Java и C++ их, соответственно, 14 и 16, так что они могут показаться даже ещё более причудливыми, чем Algol 60. Ну а в языке PERL имеется 24 уровня приоритетов операций. В этом смысле язык PERL просто расчудесен, достижение простоты языка явно не было приоритетной задачей при его разработке. В приведённой ниже таблице указаны все операции PERL вместе с соответствующими им уровнями приоритета.

Уровень приоритета Операция
  1 левосторонние списковые операции
  2 ->
  3 ++ --
  4 **
  5 ! ~ \ + (унарный) - (унарный)
  6 =~ !~
  7 * / % x
  8 + - .
  9 << >>
10 именованные унарные операции
11 < > <= >= lt gt le ge
12 == != <=> eq ne cmp
13 &
14 | ^
15 &&
16 ||
17 .. ...
18 ?:
19 = += -= *= /= %= **= &= |= ^= <<= >>= &&= ||= .= x=
20 , =>
21 правосторонние списковые операции
22 not
23 and
24 or xor

Такое изобилие операций и уровней приоритетов есть прямое следствие нетипизированной природы языка PERL, когда в одном выражении могут быть использованы объекты самых разных типов и, соответственно, самые разнообразные операции. А теперь некоторые из операций рассмотрим более подробно.

3.1. Арифметические операции

Начнём с того, что операции инкремента (++) и декремента (--) применимы не только к целым числам, то также и к вещественным числам, и, что особенно интересно, к строкам, даже тем, которые не являются строковым представлением какого бы то ни было числа. В первом случае к значению вещественного числа прибавляется единица, а второй рассмотрим подробнее, поскольку здесь имеется аж три варианта. Если первые n символов строки являются представлением числа (целого или вещественного), то в качестве операнда инкремента или декремента будет использовано соответствующее число, а символы, следующие после него (если они есть), будут отброшены. Иначе, если строка состоит только из символов, принадлежащих любым из следующих трёх классов: прописные буквы, строчные буквы, цифры, то при вычислении инкремента или декремента каждый символ ведёт себя как цифра, причём каждый символ остаётся в своём классе, а переносы учитываются. Иначе, если хотя бы один из символов строки не принадлежит ни одному из указанных трёх классов, то данная строка считается равной числу 0. Следующий фрагмент исходного кода иллюстрирует описанные выше факты:

1: $f = "46e3(*&^"; # первые 4 символа образуют вещественное число
2: print(++$f);     # будет напечатано "46001"
3: $f = "Zz9";      # только прописные, строчные буквы и цифры
4: print(++$f);     # будет напечатано "AAa0", перенос учитывается!
5: $f = "Z 8 0";    # строка содержит спецсимвол (пробел)
6: print(++$f);     # будет напечатано "1"

Нужно отметить, что во втором варианте (строки 3 и 4) операции инкремента и декремента не эквивалентны суммированию или вычитанию единицы, поскольку в случае суммирования или вычитания такого рода строки считаются равными числу 0 (как и в третьем варианте, строки 5 и 6). Возможность оперирования с подобными строками является, видимо, уникальной особенностью операций инкремента и декремента. Вообще, применимость арифметических операций к строкам и возникающие из-за этого трудноуловимые ошибки программирования является следствием нетипизированной природы PERL.

Реализация процедур инкремента и декремента в виде операций фактически приводит к совмещению понятий "операция" и "оператор" (в данном случае – оператор присваивания, более подробно это обсуждается в следующем разделе). Это, вообще говоря, плохо, поскольку операции и операторы выполняют в языке совершенно различные функции: операции, совместно с константами, переменными и функциями, используются для конструирования выражений с целью вычисления того или иного значения, а операторы используются для управления вычислением выражений. Такого рода совмещение (именно операции суммирования или вычитания с оператором присваивания) делает весьма затруднительным (если не невозможным) процесс доказательства правильности программ: "Отметим предположение, что изменяется значение только той переменной, имя которой появляется слева от знака присваивания. Не допускаются так называемые "побочные эффекты", в результате которых изменяются значения других переменных. Если данное допущение нарушается, то в общем случае не будут верными ни аксиома оператора присваивания, ни те правила вывода, которые из неё следуют" [2]. Возможно, именно по этой причине в языке Pascal процедуры инкремента и декремента реализованы именно как стандартные процедуры (inc и dec соответственно), а не функции. Для получения следующего и предыдущего значений используются стандартные функции succ и pred соответственно, но они не изменяют значений переменных.

Язык PERL поддерживает операцию возведения в степень, для неё одной предоставлен свой собственный уровень приоритета. Отметим лишь, что наличие встроенной непосредственно в язык операции возведения в степень совершенно излишне – её реализация должна быть вынесена во внешнюю библиотеку, поскольку необходимость в её использовании возникает достаточно редко и только при программировании приложений, ориентированных на сравнительно узкую предметную область (разного рода прикладные библиотеки численного характера). Это замечание становится тем более верным, что в языке PERL уже присутствуют встроенные функции exp и log, с помощью которых при желании можно было бы реализовать необходимое возведение в степень непосредственно в выражении или в виде функции, например так:

sub Power {
   my ($Base, $Power) = @_;
   return exp($Power*log($Base));
}

Кстати, встроенная функция log вычисляет натуральный логарифм, а не логарифм по произвольному основанию, как можно было бы подумать, взглянув на её имя. Видимо, разработчики PERL очень плохо учились (а может и не учились вовсе) и не знают, что в математике то, что они обозначают через log, на самом деле обозначается через ln, а через log обозначается логарифм по произвольному основанию.

Операция вычисления остатка от деления % с математической точки зрения реализована в PERL некорректно (как, впрочем, и во всех остальных известных автору языках программирования, исключая Oberon – все указанные ниже замечания не относятся к этому языку), хотя и по-другому, чем, скажем, в Pascal, Visual Basic, C или Java. Фактически, эта операция в PERL реализована точно так же, как в языках FoxPRO и MatLab. В связи с непониманием общественностью операций целочисленного деления и вычисления остатка от целочисленного деления уместно напомнить их математическое определение: "В случае, когда делимое не делится нацело на делитель, иногда выполняют так называемое деление с остатком. Деление с остатком есть отыскание наибольшего целого числа, которое в произведении с делителем даёт число, не превышающее делимое. Искомое число называется неполным частным. Разность между делимым и произведением делителя на неполное частное называется остатком; он всегда меньше делителя" [6]. Из этого определения следуют два факта:

  1. во-первых, операции целочисленного деления (обозначим div) и вычисления остатка от целочисленного деления (обозначим mod) несимметричны относительно нуля, что выражается следующими неравенствами:
  2. -x div y <> -(x div y)
    -x mod y <> -(x mod y)
  3. во-вторых, делитель должен быть положительным, поскольку в противном случае либо деление невозможно по определению (при делителе, равном нулю), либо не удаётся выполнить условие, согласно которому остаток всегда меньше делителя (при отрицательном делителе).

Однако, в нарушение данного определения, в PERL (как и во всех остальных упоминавшихся языках) разрешено использование отрицательного делителя при вычислении остатка от целочисленного деления, результаты такого рода вычислений, строго говоря, лишены всякого математического смысла:

print((-7) % (-5)); # будет напечатано "-2"
print(( 7) % (-5)); # будет напечатано "-3"

Здесь -2 > -5 и -3 > -5, а это не согласуется с требованием определения, чтобы остаток был меньше делителя: в противном случае просто теряется смысл понятия "остаток". Более того, приведённые выше результаты операции mod соответствуют следующим равенствам:

-7 div -5 =  1
 7 div -5 = -2

Здесь -5*1 = -5 > -7, а -5*(-2) = 10 > 7, что не согласуется с требованием определения, чтобы неполное частное в произведении с делителем давало число, не превышающее делимое.

Тем не менее, существуют обобщения операций div и mod, допускающие использование отрицательных делителей. Так, в [8] дано следующее определение операции mod: x mod y = x - y*floor(x/y), y <> 0. Это определение, данное без дополнительных оговорок, допускает использование не только отрицательных, но и вещественных чисел (кстати, именно в таком виде реализована операция % в языке Java). Однако в [8] из множества приведённых примеров нет ни одного, в котором такого рода обобщение (а оно, как было показано ранее, противоречит исходному определению) оказывалось бы действительно полезным. Более того, определение операции mod [10, стр. 104] противоречит тесно связанному с ним определению отношения делимости [10, стр. 125].

Справедливости ради нужно отметить, что для положительного делителя операция вычисления остатка от целочисленного деления реализована в PERL (а также в FoxPRO и MatLab) совершенно правильно, в отличие от других упоминавшихся языков, так

print((-7) % ( 5)); # будет напечатано "3"
print(( 7) % ( 5)); # будет напечатано "2"

А вот в языках Pascal, Visual Basic, C и Java операция вычисления остатка от деления реализована неправильно даже для положительного делителя, ниже приведён пример для C.

printf("%i", (-7) % ( 5)); /* будет напечатано "-2", неправильно */
printf("%i", ( 7) % ( 5)); /* будет напечатано "2", правильно */

В первом из указанных случаев результат операции mod соответствует такому равенству: -7 div -5 = -1. Здесь 5*(-1) = -5 > -7, что не согласуется с требованием определения, чтобы неполное частное в произведении с делителем давало число, не превышающее делимое. Следовательно, неверен и результат операции mod, поскольку согласно определению x mod y = x - y*(x div y).

Кстати, согласно man-странице perlop в PERL есть возможность вычислять остатки от целочисленного деления в точности так же, как в C, для этого достаточно указать директиву use integer, что по сути является побочным эффектом, так что будьте осторожны!

Ввиду отсутствия механизма типизации и, как следствие, наличия возможности применения одной и той же операции к практически любым типам данных, для конкатенации строк потребовалось ввести отдельную операцию ., поскольку применение операции сложения (+) подразумевает числовой контекст, а не строковый, с автоматическим преобразованием всех строк в соответствующие им числа с учётом обсуждавшихся выше тонкостей. Такая ситуация ещё более ярко проявляется в отношении операций сравнения, которые и рассмотрим далее.

3.2. Операции сравнения

Операции сравнения в PERL разделены на две группы: операции сравнения на неравенство и операции сравнения на равенство, каждой группе выделен свой уровень приоритета. Такое разделение весьма искусственно и в высшей степени бессмысленно, в связи с введением дополнительного уровня приоритета оно только затрудняет синтаксический анализ. В тех же поистине редчайших случаях, когда необходимо совместить в одном выражении несколько операций сравнения, вполне можно использовать скобки для указания нужной последовательности выполнения операций.

Ввиду нетипизированной природы PERL и вытекающей из этого невозможности различать строки и числа в PERL предусмотрено два варианта операций сравнения: один вариант предназначен для сравнения скаляров в так называемом числовом контексте, когда все строки преобразуются в соответствующие им числа с учётом особенностей, обсуждавшихся в предыдущем пункте, а второй вариант предназначен для сравнения скаляров в так называемом строковом контексте, когда все числа преобразуются в соответствующие им строки, поэтому программист вынужден постоянно и очень внимательно следить за контекстом, используя в зависимости от решаемой задачи тот или иной вариант, причём ситуации неправильного использования операций сравнения PERL, естественно, отследить не в состоянии, поскольку все "необходимые" преобразования типов выполняются автоматически за спиной программиста, что чревато трудноуловимыми смысловыми ошибками. Приведённый ниже фрагмент исходного кода иллюстрирует изложенное.

if (1 > 2 != 2 > 1) { # обе операции > выполнятся до операции !=
   print("OK");       # и строка "OK" будет напечатана
}

if ("abcdefgh" > 1) { # в числовом контексте "abcdefgh" == 0
   print("Эта строка никогда не будет напечатана");
}

if ("abcdefgh" gt 1) { # в строковом контексте 1 eq "1"
   print("Эта строка обязательно будет напечатана");
}

Сверх традиционных операций сравнения вроде < и > в PERL реализована операция <=>, которая выполняет сравнение в числовом контексте и возвращает -1, 0 или 1 в зависимости от того, меньше, равен или больше ли левый операнд правого. Аналогичная ей операция, предназначенная для работы в строковом контексте, – cmp. Это абсолютно лишние операции, вряд ли их присутствие в языке можно обосновать какими бы то ни было разумными доводами. Дело в том, что данные операции фактически предназначены для использования исключительно совместно со встроенной функцией sort. Автору не удалось обнаружить ни одного другого достойного примера их использования ни в документации, ни в учебниках, ни даже в исходных кодах стандартной библиотеки PERL – во всех остальных случаях (они, впрочем, немногочисленны) эти операции почему-то использовали вместо операций ==, !=, eq и ne. О том, насколько извращённой является встроенная функция sort, и сколькими побочными эффектами она чревата, можно говорить достаточно долго (интересующихся данным вопросом за подробностями отошлём к man-странице perlfunc), здесь же необходимо отметить тот факт, что при должной реализации сей функции вполне можно было бы обойтись, скажем, операциями <= и le без каких бы то ни было потерь в эффективности.

3.3. Прочие операции и особенности выражений

В этом пункте кратко рассмотрим остальные интересные операции и начнём с операций привязки (=~ и !~). Данные операции, в свете сказанного в пункте "Строки" раздела "Лексемы", представляются избыточными, тем более что их единственная цель – указать строку, которая будет сопоставлена с шаблоном. Уж если реализация операций с шаблонами не вынесена во внешние библиотеки (в этом случае все необходимые исходные данные передавались бы как параметры соответствующей функции) и включена непосредственно в язык, то неужели нельзя было определить эти операции так, чтобы их определение включало операнд-источник? Кстати, исключив операции привязки, можно было бы заодно избавиться от ещё одного из многочисленных уровней приоритета, убив таким образом сразу двух зайцев.

Согласно man-странице perlref примечательной особенностью операции разыменования ссылки -> является то, что её можно не указывать при необходимости обратиться к элементу массива или хеша, ссылка на который есть элемент другого массива или хеша, – разыменование в этом случае происходит автоматически. Эта особенность позволяет при программировании на PERL записывать обращения к элементам многомерных массивов, которые, в силу отсутствия поддержки их в языке PERL, программист вынужден конструировать как массивы ссылок на массивы данных, о чём говорилось в разделе "Типы данных, переменные и константы", в стиле языков, поддерживающих многомерные массивы непосредственно. Таким образом, запись ${MDArray[2]}->[3] эквивалентна записи $MDArray[2][3].

Операция повторения x также является лишней, поскольку её действие легко может быть запрограммировано. Единственный возможный довод в её пользу состоит в том, что она отчасти помогает бороться с крайней неэффективностью программ на PERL, впрочем, в весьма ограниченном количестве сравнительно редко встречающихся случаев, связанных с конструированием строк или массивов, состоящих из повторяющихся подстрок или одинаковых последовательностей элементов соответственно, что, в свою очередь, практически обесценивает наличие данной операции в языке, программы на котором неэффективны уже вследствие механизма интерпретации и отсутствия средств эффективного управления памятью.

Количество логических операций явно избыточно – возможности заимствованных из языка C операций ! (логическое отрицание), && (логическое умножение) и || (логическое сложение) продублированы на более низких уровнях приоритета операциями not, and и or соответственно. А логическая операция xor просто лишняя, трудно придумать хоть сколько-нибудь реальную ситуацию, когда её использование было бы оправдано, а даже если таковая и найдётся, то нужного результата легко достичь с помощью остальных трёх операций, образующих, как известно, избыточную функционально полную систему. Кстати, автору не удалось обнаружить ни одного примера использования операции xor ни в документации, ни в исходных текстах стандартной библиотеки PERL, что легко проверить, выполнив поиск строки "xor" в файлах .pm каталога lib. Таким образом, избавившись от последних четырёх операций, можно было бы заодно и сократить количество уровней приоритета на три.

Операции диапазона (а их целых две) также лишние – вместо них обеих гораздо лучше было бы реализовать такой полезный тип данных, как множество, избавившись таким образом от ещё одного уровня приоритета.

Лишней является и заимствованная из языка C триарная условная операция ?: вместе с соответствующим ей уровнем приоритета: в подавляющем большинстве случаев для осуществления ветвления следует использовать условный оператор if, при необходимости же выполнить анализ значения выражения непосредственно в другом выражении программист может определить, например, собственную функцию. Если уж разработчики PERL так стремились включить эту возможность непосредственно в язык, то, опять же, можно было бы это сделать с помощью встроенной функции, как это сделано, например, в языке FoxPRO (функция iif), а не вводить ещё одну лишнюю операцию вместе с ещё одним лишним уровнем приоритета.

Заимствованная из языка C операция ,, возможности которой ещё и продублированы аналогичной операцией =>, есть nonsense, потребовавший, как и в языке C, введения дополнительного уровня приоритета. Надо заметить, что эта совершенно бесполезная, бессмысленная и просто глупая операция, предназначенная, главным образом, для обеспечения возможности нагромождения возможно бо́льшего количества выражений непосредственно в условии цикла for с целью затруднить понимание программы другими людьми (подобный стиль программирования, к сожалению, моден до сих пор), сохранилась с далёких 1970-х во всех современных языках программирования, разработанных на основе языка C, включая C++ и Java.

При анализе выражений, порой встречающихся в программах на языке PERL, не искушёнными в этом языке людьми может возникнуть недоумение в связи с использованием вызова функции в левой части оператора присваивания. Это чистая правда: PERL позволяет использовать две из встроенных функций в качестве леводопустимых выражений, это функции substr и keys. Первая, вообще говоря, предназначена для выделения подстроки из строки, но если первый её аргумент есть леводопустимое выражение, то и сам вызов функции считается леводопустимым выражением, что может быть использовано для замены выделенной подстроки на другую подстроку прямо в исходной строке (при этом, в зависимости от конкретных значений, исходная строка может быть увеличена или уменьшена в длине), например так:

$s = "abc";
substr($s, 1, 1) = "po"; # а ведь можно не извращаться
                         # и использовать такую форму: substr($s, 1, 1, "po");
print($s);               # печать строки "apoc"

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

Функция keys, предназначенная для получения списка всех ключей хеша, в качестве леводопустимого выражения может быть использована для выделения дополнительных ячеек в хеш-таблице. Новый размер хеш-таблицы всегда округляется вверх до ближайшей степени двойки. Однако этим способом можно только увеличить размер хеш-таблицы, а уменьшить нельзя. Для освобождения выделенных таким образом ячеек необходимо уничтожить весь хеш с помощью встроенной функции undef. Приведённый ниже фрагмент исходного кода иллюстрирует изложенное.

%h = ("boo" => 1);
print(scalar(%h)); # будет напечатано "1/8"
keys(%h) = 150;
print(scalar(%h)); # будет напечатано "1/256" - новый размер округлён до 2**8
$h{"baa" => 2);
print(scalar(%h)); # будет напечатано "2/256" -
                   # для нового элемента используется уже выделенная ячейка
delete($h{"baa"});
print(scalar(%h)); # будет напечатано "1/256" -
                   # информация удалена, но память не освободилась
keys(%h) = 0;      # этот способ не работает
print(scalar(%h)); # снова "1/256"
undef(%h);         # нужно так
print(scalar(%h)); # будет напечатано "0"

Опять же непонятно, какая необходимость была в том, чтобы реализовать возможность управления размером хеш-таблицы именно таким изощрённым способом? Почему нельзя было сделать это в виде обычной встроенной функции?

4. Операторы

Язык PERL обладает явно избыточным и, порой, запутанным синтаксисом. Продемонстрировать это – главная цель настоящего раздела. А во введении хотелось бы обратить внимание на некоторые общие особенности операторов в PERL.

В PERL, как и в C, понятия оператора и выражения почти неразличимы, поскольку выражение может быть в любом месте, где может быть оператор (обратное верно только для операторов присваивания), а любой оператор возвращает какое либо значение. Так, результатом выполнения оператора присваивания будет присвоенное значение (в скалярном контексте) или количество удачно скопированных элементов массива (в списковом контексте), а результатом блока операторов (например, в операторах if, for, while, при выполнении функций) будет результат последнего оператора, выполненного в блоке. Таким образом, следующие строки вполне законны и не вызовут ошибок ни на этапе компиляции, ни на этапе выполнения:

"Hallo ".", "."Larry Wall!";
2 + 2;
1; # так почти всегда завершаются модули PERL -
   # указывается код, возвращаемый в ОС или в импортирующий модуль

И вообще, можно, написав любое слово, что придёт в голову, поставить символ ";" в конце строки, и это будет считаться правильным выражением/оператором, например так:

I_hate_PERL_-_what_a_tricky_language;

Секрет прост – слова, не поддающиеся никакой интерпретации, PERL считает строками, заключёнными в кавычки, в результате чего получается выражение, а это уже допустимый оператор, о как! Таким образом можно, например, вызвать несуществующую процедуру, просто опечатавшись при наборе её имени, и потратить много-много часов на отладку неработающего фрагмента – ведь вы не дождётесь от PERL сообщения об ошибке или хотя бы предупреждения ни на этапе компиляции, ни на этапе выполнения, так что ошибку придётся искать самостоятельно.

4.1. Операторы присваивания

В PERL определено аж 16 видов операторов присваивания. Вот они:

= += -= *= /= %= **= &= |= ^= <<= >>= &&= ||= .= x=

Если сюда ещё прибавить операции инкремента и декремента, выполняющие "скрытую" модификацию значения переменной, то получится 18, – немыслимое число. Разработчики PERL, видимо, постарались смешать оператор присваивания со всеми операциями, какими только возможно. Ничего, кроме путаницы, в программу это не вносит, тем более что такое смешение, позаимствованное у языка C, изначально было придумано с тем, чтобы при его использовании компилятор мог построить более эффективный код (непонятно, правда, за счёт чего). При использовании же языка PERL, который является интерпретируемым, об эффективности не может быть и речи вне зависимости от того, сколько там способов осуществить присваивание и насколько они эффективны, ведь PERL не в состоянии даже предварительно откомпилировать исходный текст в псевдокод, так что "компиляция" происходит при каждом запуске программы.

4.2. Операторы ветвления

В PERL существует целых 6 способов осуществить ветвление. Начнём с самых примитивных, взятых на вооружение у Bourne Shell:

# Вызов SomeFunction2 произойдёт, только если
# вызов SomeFunction1 возвращает true
# (если только то, что считается в PERL true, можно назвать true):

SomeFunction1() && SomeFunction2();

# Вызов SomeFunction2 произойдёт, только если
# вызов SomeFunction1 возвращает false:

SomeFunction1() || SomeFunction2();

Такой способ ветвления возможен именно благодаря тому, что любое выражение в PERL может выступать в качестве оператора. А вот ещё один, позаимствованный из C:

$Result = (IsSomeCondition() ? SomeFunction1() : SomeFunction2());

И ещё пара, чисто в стиле PERL:

# Присваивание будет выполнено, только если
# вызов IsSomeCondition возвращает true:

$a = SomeFunction() if IsSomeCondition();

# Значение $b будет напечатано, только если
# вызов IsSomeCondition возвращает false:

print($b) unless IsSomeCondition();

В данном случае if и unless – это так называемые single modifiers, которые могут следовать после любого выражения или оператора присваивания. И, наконец, наиболее полная форма условного оператора:

if (IsSomeCondition1()) {     # здесь блок - не пижонство,
   SomeFunction1();           # а жизненная необходимость
} elsif(IsSomeCondition2()) { # секция elsif необязательна,
   SomeFunction2();           # их может быть много
} else {                      # секция else также необязательна
   DefaultAction();
}

Следует отметить, что при всём многообразии способов ветвления оператор switch (для любителей C) или case (для любителей Pascal) отсутствует, его предлагается моделировать самостоятельно с помощью метки, блока и набора условных операторов внутри него с безусловным переходом к концу блока в каждом из условных операторов – это, между прочим, описано в man-странице perlsyn. Большей глупости трудно придумать, кому и зачем это надо – непонятно, ведь есть if-elsif-else. Тем более, что оператор switch/case был придуман не столько для того, чтобы осуществить множественное ветвление, сколько для того, чтобы в случае множественного ветвления по набору констант можно было построить более эффективный код. При компиляции оператора swith/case компиляторы C и Pascal, соответственно, строят высокоэффективные таблицы переходов, так что код с одним оператором swith/case выполняется много быстрее (чем больше альтернатив – тем больше выигрыш), чем набор вложенных операторов if. Задача обеспечения эффективности на этапе выполнения, видимо, была далеко не самой приоритетной при разработке PERL, число же возможностей для выполнения обычного ветвления явно избыточно – вполне можно было обойтись одним оператором if-elsif-else.

4.3. Операторы цикла

Операторов цикла ещё больше – целых 9. Начнём с наиболее традиционных – for и while. Первый является точным аналогом оператора for языка C:

for ($i = 10; $i > 0; $i--) { # стиль C "три в одном":
                              # инициализация, условие продолжения, инкремент
   SomeFunction();
}

Второй несколько оригинальней:

$i = 10;         # инициализация

while ($i > 0) { # условие продолжения
   SomeFunction();
} continue {     # инкремент, этот блок необязателен
   $i--;
}

Приведённый здесь в качестве примера для цикла while код полностью эквивалентен примеру для цикла for выше. Блок continue, если он присутствует, выполняется всякий раз после выполнения тела цикла. Он используется, главным образом, при осуществлении безусловных переходов, предназначенных для управления выполнением цикла.

А вот ещё одна форма цикла for – оператор foreach, позаимствованный, наверное, у Bourne Shell, он подобен оператору foreach языка Java Script или For Each языка Visual Basic:

foreach $Element (@Array) { # элементы массива @Array последовательно
                            # присваиваются переменной $Element,
                            # размер массива может быть неизвестен

   Process($Element);       # обработка очередного элемента массива
}

Указание переменной, которой присваивается значение очередного элемента массива, необязательно, в случае её отсутствия присваивание идёт встроенной переменной $_. Поскольку сам по себе язык PERL не в состоянии обеспечить контроль выхода индекса массива за пределы допустимого диапазона (в данном случае массив автоматически переопределяется), это возложено на цикл foreach, который рекомендуется использовать всегда, когда необходимо последовательно просмотреть весь массив, хотя этого же результата без потерь надёжности (если вообще возможно говорить о надёжном программировании на PERL) вполне можно достичь и с помощью цикла for:

for ($i = $#Array; $i >= 0; $i--) {
   Process($Array[$i]);
}

Существует, кроме того, пара single modifiers, которые, в зависимости от способа использования, моделируют либо цикл с пред-, либо цикл с постусловием. Рассмотрим первый вариант:

$i = 10; # инициализация

print(" ".$i--) while $i > 0; # печать строки " 10 9 8 7 6 5 4 3 2 1"
print(++$i." ") until $i > 9; # печать строки "1 2 3 4 5 6 7 8 9 10 "

В данном случае условие single modifier вычисляется всегда до вычисления выражения, после которого он следует, даже для single modifier until. Во втором варианте – наоборот, то есть сначала вычисляется выражение, а затем уже – условие single modifier, даже для single modifier while:

$i = 10; # инициализация

do {
   print(" ".$i--);
} while $i > 10; # перед выполнением цикла данное условие ложно,
                 # тем не менее, будет выполнена печать строки "10"

do {
   print(++$i." ");
} until $i < 10; # перед выполнением цикла данное условие истинно,
                 # однако цикл начнёт своё выполнение и никогда не остановится

Несмотря на то, что do выглядит как "сложное предложение", вроде операторов if или while, на самом деле это – "простое предложение", вроде выражения или оператора присваивания, поэтому к нему применимы single modifiers.

Кроме while и until, есть ещё один single modifier, позволяющий циклически вычислять выражение:

print("$_\n") foreach @Array; # печать элементов массива @Array в столбик

И, наконец, последний способ организации цикла:

$i = 0;         # инициализация

Loop: {
   print($i++); # печать с инкрементом ("два в одном")
   redo Loop;   # оператор управления циклом
}               # а теперь нажмите Ctrl + C

А вот этот код не работает так, как ожидается, хотя согласно man-странице (именно perlsyn), описывающей синтаксис операторов цикла, блок continue может следовать после обычного блока и, по идее, должен использоваться оператором next:

$i = 0;       # инициализация

Loop: {
   print($i);
   next Loop; # переход не работает
} continue {  # а вот инкремент выполняется - только один раз
   $i++;
}

В данном случае возможность описания блока continue, который всё равно не работает, ничего, кроме путаницы, не даёт. Зачем это нужно – вообще не понятно.

Автор сих строк считает, что в языке программирования должен быть один и только один оператор цикла – оператор while, причём "операторы управления циклом" должны быть исключены как разновидность безусловного перехода, нарушающего соответствие между последовательностью операторов текста программы и последовательностью их исполнения. На худой конец, можно согласиться с тем, что предложено, скажем, в языке Pascal. Автор считает неприемлемым уже то, как реализованы циклы в Java, с его переходами по меткам (иногда возникает впечатление, что на дворе 60-е). Что же касается PERL – здесь ребята явно перебрали (за интерпретацию последнего слова автор ответственности не несёт). Совершенно непонятно, например, зачем было дублировать возможности операторов while и foreach с помощью single modifiers, а полезность блока continue более чем сомнительна. Помимо явной избыточности, усложняющей интерпретатор, такое разнообразие стимулирует написание с трудом читаемых программ, когда то здесь, то там циклы реализованы то так, то сяк. Это особенно актуально, когда приходится отлаживать код, написанный другими людьми, тем более если они вовсю используют все те 9 конструкций.

4.4. Операторы безусловного перехода

На тему оператора безусловного перехода было написано очень многими, очень много и проблемы, им порождаемые, были известны очень давно. Зря старались. Всё это, как видно, прошло мимо разработчиков PERL. Поэтому в данном языке существует 4 разных оператора безусловного перехода, причём самый опасный из них реализован аж в трёх "ипостасях". Однако, обо всех по порядку.

Начнём с операторов управления циклом, их три: next, last и redo. Они могут использоваться только внутри блоков операторов цикла, но на самом деле – везде, где вздумается, поскольку обычный блок – это цикл, выполняющийся один раз, так что эти операторы можно применять внутри любого обычного блока. По сравнению с аналогичными операторами языка C они расширены возможностью указания метки – цели перехода, в качестве которой может выступать не только тот оператор цикла/блок, который непосредственно содержит данный оператор управления циклом, но также и внешний оператор цикла/блок, который содержит данный, – этим, так сказать, "ограничивается" действие операторов управления циклом. При этом если метка не указана, то подразумевается оператор цикла/блок, который непосредственно содержит данный оператор управления циклом. Оператор goto и сотоварищи нынче в моде – подобный бардак имеет место и в Java.

Оператор next подобен оператору continue языка C, он приводит к повторению цикла сначала с выполнением блока continue (если он присутствует) и проверкой условия цикла:

Loop: while (IsSomeCondition1()) {
   DoSomething1();
   next Loop if IsSomeCondition2();
   DoSomething3(); # не будет выполнено,
                   # если вызов IsSomeCondition2 возвращает true
} continue {       # этот блок будет выполняться при каждой итерации
   DoSomething2();
}

Если целью перехода оператора next является внешний блок, то выполняется continue-блок этого внешнего блока, а не того, который содержит оператор next.

Оператор last подобен оператору break языка C, он прекращает выполнение цикла и передаёт управление оператору, непосредственно следующим за тем блоком, метка которого указана в качестве параметра, а условие цикла не проверяется, и блок continue не выполняется, даже если он присутствует:

Loop: while (IsSomeCondition1()) {
   last Loop if IsSomeCondition2();
   DoSomething1();
} continue { # этот блок не будет выполняться при выходе из цикла через last
   DoSomething2();
}

# last передаёт управление сюда

Оператор redo приводит к повторению цикла сначала без выполнения блока continue и без проверки условия цикла:

$i = 0;         # инициализация

Loop: {
   print($i++); # печать с инкрементом ("два в одном")
   redo Loop;
}               # а теперь нажмите Ctrl + C

Наконец, рассмотрим оператор goto. Этот оператор используется с одним обязательным параметром, который может быть:

  1. идентификатором, указывающим метку – цель перехода:
  2. goto SomeLabel;
  3. выражением, возвращающим строку – имя метки, цели перехода:
  4. goto ($SomeLabel1, $SomeLabel2, $SomeLabel3) [$i];
  5. вызовом функции, что позволяет, при использовании внутри подпрограммы, "притвориться" для вызывающей (под)программы, будто была вызвана не та подпрограмма, внутри которой использован оператор goto, а та, имя которой указано в качестве параметра:
  6. goto &SomeFunction();

Наличие первой из указанных форм оператора goto уже плохо, но наличие таких извращённых форм его, как вторая и третья, – это вообще nonsense, способный полностью разрушить структуру программы и сделать её практически несопровождаемой. Скажем, вторая форма не позволяет PERL проверить достижимость метки на этапе компиляции, так что в случае ошибки её нужно будет искать с помощью отладчика. Необходимость же в третьей форме goto неясна, наверное, даже самим разработчикам PERL (как отмечено в man-странице perlsyn – эта форма "is highly magical"2), она, как правило, используется в блоках AUTOLOAD при так называемой "автозагрузке" пользовательских функций для того, чтобы скрыть сам факт автозагрузки. Зачем – неизвестно. Справедливости ради нужно отметить, что в руководствах по PERL не рекомендуется использовать эти операторы. Ещё бы, ведь "вряд ли может вызвать какие-либо сомнения утверждение, что использование оператора goto, разрывающего связь между последовательностью действий и текстуальной структурой программы, часто является источником ошибок и затрудняет отладку программ" [5]. А тогда сразу встаёт вопрос, зачем он присутствует в языке? Наверное, чтобы была возможность быстро "залатать" дыру, возникшую в проекте как результат плохого проектирования.

В заключение этого раздела автору очень хотелось бы процитировать замечательные слова статьи О. Лекарма и П. Дежардена "Дополнительные замечания по поводу языка программирования Pascal": "… составление бесконечного списка конструкций не тот путь, по которому стоит идти при разработке новых (более хороших, чем старые) языков программирования. Самой неудачной попыткой движения в этом направлении является создание языка PL/1, хотя неисправимые поклонники и энтузиасты-адепты языка PL/1 требуют включения в него всё новых и новых конструкций" [9]. Но, несмотря на это, "программистский мир всё время жаждет более мощных языков, а вовсе не ограниченных подмножеств" [11]. В связи с изложенным в данном разделе заявление "PERL is in many ways a simple language"3 [1] может быть воспринято как откровенное издевательство.

5. Подпрограммы

Все подпрограммы в PERL являются подпрограммами-функциями, возвращающими какое-либо значение, даже если это не определено явно, поэтому далее подпрограммы называются просто функциями.

5.1. Синтаксис определения и вызова подпрограмм

Синтаксис определения функций прост до убожества, так что не только программисту, но и компилятору трудно понять, какие параметры должны передаваться и что должно быть возвращено. Вот типичный пример:

sub SomeFunction {
   # тело функции
}

С точки зрения программиста, о функции необходимо знать всего три вещи:

  1. имя функции;
  2. количество принимаемых параметров, их имена, типы и порядок передачи;
  3. тип возвращаемого значения.

Первая и вторая необходимы для правильного вызова функции, а третья – для правильного использования результата, возвращаемого функцией. В языках высокого уровня традиционно присутствуют средства описания этой информации средствами самого языка, поскольку это, с одной стороны, позволяет повысить надёжность кода за счёт автоматических проверок, выполняемых на этапе компиляции, а с другой – упростить документирование, сделав код самодокументированным. Ясно, что правильно выбранные идентификаторы функции и параметров косвенно описывают назначение функции и способ её вызова. Однако эти средства отсутствуют в PERL, таким образом, описания функций на языке высокого уровня PERL не более информативны, чем, скажем, описания подпрограмм на языке ассемблера. О проверках же на этапе компиляции и вовсе говорить не приходится, о чём подробнее рассказано далее.

Функции вызываются по имени, после которого следует список параметров, разделяемых запятыми. Здесь интересно то, что, во-первых, список параметров можно не заключать в круглые скобки, а во-вторых, перед именем функции можно указывать символ "&", указывающий так называемый контекст, обозначающий, что это именно вызов функции, а не обращение к переменной или что-либо ещё:

$a = &SomeFunction 1, "blah-blah-blah", \%MyHash;

В ранних версиях PERL символ "&" нужно было указывать при каждом вызове функции, однако теперь это, наконец, стало необязательным, однако при вызове без этого символа PERL, возможно, будет выполнять кое-какие дополнительные проверки на этапе компиляции, которые, впрочем, способны только сбить с толку и привести к появлению дополнительных сюрпризов в программе, о чём будет подробнее сказано далее. В любом случае, наличие нескольких возможностей для реализации даже такой простой вещи, как вызов функции, совершенно ничем не оправдано и приводит к путанице.

Наиболее опасно с точки зрения надёжности отсутствие необходимости заключать список параметров в скобки, поскольку это может привести к трудноуловимым смысловым ошибкам, которые могут быть устранены только на этапе выполнения при отладке. Вот несколько примеров:

print 1 + 2 + 3;   # печать "6"
print (1 + 2) + 3; # печать "3", а не "6"
print(1 + 2 + 3);  # печать "6"

Поэтому такую возможность использовать не рекомендуется, а программировать следует по возможности так, как это делается в здоровых языках типа Pascal, то есть список параметров заключать в круглые скобки, хотя, естественно, за этим необходимо следить самостоятельно, PERL не будет это контролировать.

Но самым интересным является тот факт, что PERL не в состоянии обнаружить отсутствия вызываемой функции на этапе компиляции. Автор сам пал жертвой такой подлости (иначе и не назовёшь), опечатавшись однажды и написав retirn $Result; вместо нужного return $Result;.

5.2. Передача параметров и возврат значений

Все функции в PERL являются функциями с переменным числом параметров (обязательных параметров не существует), что в сочетании с отсутствием механизма типизации исключает не только возможность выполнить проверку правильности вызова функции на этапе компиляции, но даже возможность описать средствами самого языка то, как следует её вызывать. Это приводит, с одной стороны, к большим трудностям при отладке, поскольку подавляющее большинство ошибок при вызове функций может быть обнаружено только на этапе выполнения, а с другой – к необходимости чётко документировать каждую, даже самую простую функцию, так как в противном случае разобраться в исходном коде через некоторое время становится практически невозможно.

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

Все параметры, являющиеся глобальными (по отношению к функции) переменными, в функции передаются по ссылке, а константы, естественно, по значению как элементы встроенного массива @_. Это, с одной стороны, исключает возможность гарантировать неизменность глобальных переменных при передаче их значений в функции уже на этапе компиляции, а с другой – лишает возможности назначить формальным параметрам функции мнемоничные символические имена. Кроме того, ни на этапе компиляции, ни на этапе выполнения невозможно проконтролировать, была ли передана ссылка на переменную или константа. Для хоть какого-то разрешения этих проблем обычно используется банальное копирование переданных значений в локальные переменные (об областях видимости идентификаторов мы поговорим чуть позже):

sub SomeFunction {
   my ($Parameter1, $Parameter2) = @_;

   # теперь вместо $_[0] и $_[1] можно использовать
   # $Parameter1 и $Parameter2 соответственно,
   # при этом гарантируется, что, если переданные параметры являются
   # глобальными переменными, то они не изменятся при присваиваниях
   # $Parameter1 и $Parameter2 внутри функции
}

Следует обратить внимание на то, что при копировании в локальные переменные, фактически, значений из массива @_, содержащего непонятно что (ссылки или значения), не требуется разыменования. Исключением является случай, когда требуется получить доступ к данным, переданным по ссылке явно, то есть когда соответствующий элемент массива @_ содержит не ссылку на данные, а ссылку на ссылку на данные.

Присваивание элементу массива @_, содержащего ссылку на глобальную переменную, изменит значение этой переменной, и это изменение останется в силе после возврата из функции, а если этот элемент содержит значение константы, то при присваивании возникнет ошибка времени выполнения. При присваивании также не требуется разыменования. При этом, естественно, возможности обратиться к параметру по символическому имени нет, так что придётся писать что-то вроде

sub SomeFunction {
   $_[0] = 123; # о типе переданного параметра должен заботиться разработчик
}

Начиная с версии 5.002 язык PERL позволяет при описании функции указывать так называемый "прототип", то есть шаблон, которому должны соответствовать переданные параметры. Шаблон состоит из последовательности символов "$" (обозначает скаляр), "@" (обозначает массив), "%" (обозначает хеш), перед любым из них может стоять символ "\", обозначающий ссылку на соответствующую структуру данных. Можно также использовать символ "*", который обозначает элемент таблицы символов. Обязательные параметры должны быть сгруппированы вначале и отделяются от необязательных символом ";". Вот примеры, приведённые в man-странице perlsub:

Объявление               Пример вызова

sub mylink ($$)          mylink $old, $new
sub myvec ($$$)          myvec $var, $offset, 1
sub myindex ($$;$)       myindex &getstring, "substr"
sub mysyswrite ($$$;$)   mysyswrite $buf, 0, length($buf) - $off, $off
sub myreverse (@)        myreverse $a, $b, $c
sub myjoin ($@)          myjoin ":", $a, $b, $c
sub mypop (\@)           mypop @array
sub mysplice (\@$$@)     mysplice @array, @array, 0, @pushme
sub mykeys (\%)          mykeys %{$hashref}
sub myopen (*;$)         myopen HANDLE, $name
sub mypipe (**)          mypipe READHANDLE, WRITEHANDLE
sub mygrep (&@)          mygrep { /foo/ } $a, $b, $c
sub myrand ($)           myrand 42
sub mytime ()            mytime

Проверки на предмет соответствия вызова функции объявленным прототипам выполняются компилятором весьма наивным образом, а при несовпадении контекста он автоматически производит преобразования по правилам, обсуждавшимся ранее в разделе "Типы данных, переменные и константы", так что реально можно проконтролировать только количество переданных параметров, да и то не всегда: если в прототипе указан символ "@" или "%", то все остальные символы прототипа, следующие за ним, во внимание не принимаются. Более того, все эти приносящие мало пользы и ещё более запутывающие при сопровождении программ проверки будут выполнены, только если при вызове функции не указан символ "&", если же он указан, то всё работает так, будто прототипа и вовсе нет.

Результатом выполнения функции, который возвращается в вызывающую программу, считается результат выполнения последнего оператора. Как уже говорилось ранее, каждый оператор в PERL, подобно выражению, возвращает некоторое значение – результат выполнения. Для принудительного выхода из функции можно использовать оператор return, заимствованный из C, необязательный параметр которого указывает возвращаемое значение.

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

Непонятно, зачем это Larry Wall так сильно старался изобрести велосипед, когда тот не только давно изобретён, но и уже вытеснен более совершенными видами транспорта? Язык Pascal, обеспечивающий здоровые средства описания подпрограмм, их вызова и автоматического контроля соответствия вызовов подпрограмм их описаниям, причём на этапе компиляции, был создан ещё в конце 1960-х годов. Как видно, автор языка PERL мало знаком с тем, что было достигнуто виднейшими учёными и ведущими специалистами в области языков и технологий программирования за последние 40 лет. Ему, наверное, и не снилось то, что грамотные программисты всего мира освоили уже очень давно. Впрочем, это неудивительно, ведь язык PERL, насколько это известно, разрабатывался средствами языка C, весьма и весьма далёкого от совершенства в плане обеспечения надёжности и дисциплины при программировании. А результат плачевен: вместо того, чтобы раз и навсегда реализовать ясные и чётко определённые средства описания и вызова подпрограмм, язык PERL по мере своего "развития" обогащается всё новыми и новыми средствами делать разные трюки.

5.3. Область видимости идентификаторов

Для объявления внутри функции локальных переменных существует 2 способа. Первый (устаревший) заключается в использовании встроенной функции local, а второй (рекомендуется) – в использовании встроенной функции my. Первая в области видимости текущего блока создаёт переменные, сохраняя значения глобальных переменных (которые становятся недоступными) при совпадении имён, доступные не только внутри данного блока и всех вложенных блоков, но также и внутри всех вызываемых в данном блоке функций, как будто эти переменные были глобальными. Вторая создаёт переменные, доступные только в текущем блоке и вложенных блоках, при этом доступ к глобальным переменным, имена которых совпали с локальными, возможен с помощью квалифицированных идентификаторов. Как local, так и my можно применять внутри любого блока, не только внутри функции. При выходе из блока, внутри которого были созданы локальные переменные, их значения теряются и при повторном выполнении блока они не восстанавливаются (впрочем, есть исключения). Ниже приведён пример для функции local:

sub f1 {
   local $i = 20;
   &f2;
}

sub f2 {
   print($i);       # печать строки "20" -
                    # local-переменные доступны в вызываемых функциях
   print($main::i); # опять "20" -
                    # local-переменная в этом блоке ведёт себя как глобальная
}

$i = 10;
&f1;
print($i); # печать строки "10" -
           # исходное значение глобальной переменной сохранилось

А вот пример для функции my:

sub f1 {
   print($i);
}

$i = 20;

{
   my $i = 10;

   {
      print($i);       # печать строки "10" -
                       # my-переменные доступны из вложенных блоков
      print($main::i); # печать строки "20" -
                       # глобальные переменные тоже доступны
   }

   &f1; # печать строки "20" - my-переменные недоступны из вызываемых функций
}

print($i); # печать строки "20" - значение глобальной переменной не изменилось

Наличие своей области видимости для каждого блока – излишество, вполне достаточно двух областей видимости: область видимости модуля и область видимости подпрограммы. Наличие же local-переменных в совокупности с отсутствием в PERL жёсткого требования объявлять переменные перед их использованием и вовсе опасно. Таким образом можно, например, забыв объявить и инициализировать локальную переменную внутри вызванной функции, нечаянно модифицировать значение local-переменной внешней функции, не получив об этом никаких сообщений от компилятора.

6. Модульное программирование

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

6.1. Синтаксис определения модуля

Хотя в языке PERL и существует понятие модуль (package), синтаксис соответствующей конструкции и, вообще, само понятие определить чётко (в виде РБНФ, например) в общем случае довольно сложно. Дело в том, что, в отличие, скажем, от языков с традиционно развитыми средствами модульного программирования, вроде Modula и Oberon, в которых модуль – это статическая конструкция, где каждый элемент имеет своё, чётко определённое назначение и место, в PERL модуль – это, скорее, даже не законченная синтаксическая конструкция, а некий конгломерат объявлений, функций, операторов, выражений, директив импорта и тэгов, причём всё это беспрепятственно может быть перемешано в такую кашу (а на практике это не редкость), разобраться в которой будет очень непросто. Примеры такого стиля программирования интересующиеся легко могут обнаружить в стандартной библиотеке PERL, они свидетельствуют лишь о том, что PERL нисколько не способствует повышению дисциплины при программировании, а даже наоборот, поощряет, стимулирует, буквально провоцирует написание программ, не поддающихся никакому сопровождению. Всё это усугубляется наличием дополнительных модулей, "облегчающих", "автоматизирующих", да и просто (чего уж там скрывать) "поддерживающих" дополнительные возможности модульного программирования.

Не следует думать, что модуль – это обязательная конструкция для оформления программ на PERL. Если, скажем, о языке Oberon можно сказать, что любая программная система, написанная на нём, есть совокупность модулей, то в отношении языка PERL такого сказать нельзя: программная система на нём в общем случае состоит из модулей, классов (рассматриваемых как некая разновидность модуля – о классах чуть позже) и скриптов (файлов, содержащих тело модуля без заголовка).

Вот номинальный синтаксис оформления модуля:

package SomeModule;
   # Всё, что следует после заголовка модуля, считается телом модуля,
   # которое оканчивается заголовком другого модуля или концом файла
   # Можно, хотя это необязательно, в конце модуля поставить тэг __END__
   # или тэг __DATA__ (выбирайте сами), после которых и до конца файла
   # любой текст будет проигнорирован компилятором (правда, в этом случае
   # вам не удастся описать ещё один модуль в этом же файле)
   # Разработчики PERL так и не сумели определить путный терминатор
   # модуля, вроде ключевого слова end

Недурно, правда? В модулях PERL синтаксисом не предусмотрены, как это сделано в Modula и Oberon, специальные разделы для описания списков импорта и экспорта, типов, констант, переменных, функций, инициализации. Блоки инициализации и завершения, директивы импорта, операторы и выражения, описания функций могут следовать в любом порядке практически без ограничений, а большая часть ошибок, неизбежно возникающих при таком "программировании", может быть обнаружена только на этапе выполнения. Переменные могут использоваться до их объявления и инициализации, а функции – до их описания, и такие ситуации не контролируются компилятором – все они обрабатываются на этапе выполнения.

Всё убожество языка PERL, вся его нелепость и глупость очень ярко проявляются хотя бы в том факте, что PERL допускает множественное определение функции с одним и тем же именем в области видимости одного модуля. При вызове функции по этому имени управление будет передано той, что была описана последней, а сообщение об ошибке не появится ни на этапе компиляции, ни на этапе выполнения. Данная "особенность", совместно с неспособностью PERL проконтролировать наличие вызываемой функции на этапе компиляции, способна, мягко говоря, несколько осложнить отладку. Автор однажды сам попался на эту удочку, когда, после внесения в один и тот же модуль изменений разными разработчиками независимо друг от друга и последующего слияния, в итоговом модуле оказались две версии одной и той же функции, причём ошибочная версия, естественно, оказалась последней. Зато мы провели тогда удивительный вечер.

Кстати, данную ситуацию не следует путать с механизмом перегрузки функций в стиле C++ – существование данного механизма, основанного на различиях в описании принимаемых перегруженными функциями параметров, просто исключено в PERL, поскольку, как уже указывалось в разделе "Подпрограммы", средства описания функций PERL слишком примитивны, чтобы позволить программисту определить принимаемые функцией параметры средствами самого языка – это удел документации (если она вообще есть).

В PERL существует несколько имён, которые имеют предопределённое назначение и используются интерпретатором во время выполнения для тех или иных целей. Здесь рассмотрим три, предназначенных для описания функций: BEGIN, END и AUTOLOAD. Поскольку ключевое слово sub при этом указывать необязательно, то они часто называются не функциями, а блоками.

Блоки BEGIN и END – это, соответственно, блоки инициализации ("конструкторы") и завершения ("деструкторы") модуля. "Конструкторы" выполняются сразу же, как только они обработаны компилятором, даже не дожидаясь разбора остальной части модуля. Это, как правило, происходит на этапе загрузки модуля в память при запуске программы (если модуль импортирован директивой use) или при динамической компиляции и загрузке в память на этапе выполнения программы (если модуль импортирован директивой require). "Деструкторы" выполняются при завершении работы интерпретатора. В одном модуле может быть описано множество блоков BEGIN и END, но, в отличие от пользовательских функций, все они в нужное время будут вызваны интерпретатором, причём блоки BEGIN вызываются в порядке их описания, а блоки END – в порядке, обратном порядку их описания в модуле. Такой порядок вызова принят для того, чтобы соответствующие "конструкторы" и "деструкторы" можно было группировать парами и обеспечить при этом вызов "деструкторов" в обратном "конструкторам" порядке:

BEGIN {
   # вызван первым
}

END {
   # вызван последним
}

BEGIN {
   # вызван вторым
}

END {
   # вызван предпоследним
}

...

Наличие блока инициализации модуля понятно, но почему, спрашивается, для него нет чётко определённого синтаксисом места? Почему язык допускает, чтобы код инициализации был разбросан по всему модулю в виде множества блоков? Разве может быть в этом хоть какая-то объективная необходимость? Следующий код, например, вполне законен, но может запросто ввести в заблуждение:

if (0) {
   BEGIN {
      print("Ку-ку");
   }
}

Наличие же "деструкторов" и вовсе бессмысленно – при завершении работы интерпретатора все ресурсы будут освобождены автоматически либо самим интерпретатором, либо операционной системой.

Блоки AUTOLOAD были введены в язык как средство борьбы с неэффективностью запуска программы, возникающей при большом количестве модулей в ней. Дело в том, что PERL не позволяет откомпилировать модули предварительно в псевдокод, а затем использовать уже откомпилированные версии для запуска и выполнения программы. Вместо этого компиляция модулей происходит при каждом запуске программы (если модуль импортирован директивой use) или в процессе выполнения (если модуль импортирован директивой require). Если определение вызываемой функции в модуле не найдено, то управление будет передано блоку AUTOLOAD (если он есть – в противном случае произойдёт ошибка этапа выполнения), в качестве параметров ему будут переданы те, что были указаны при вызове функции, а встроенная переменная $AUTOLOAD будет содержать квалифицированное имя вызываемой функции.

Блок AUTOLOAD может обрабатывать такие вызовы по своему усмотрению, в частности, загружая на этапе выполнения определения необходимых функций из внешних файлов. На механизме автозагрузки основана работа стандартных модулей AutoLoader и SelfLoader, которые позволяют, разделив описания функций одного модуля по разным файлам, загружать их по требованию. На сколько это способно повысить эффективность – вопрос спорный. Ведь подобные ухищрения не избавляют от главной проблемы – невозможности предварительной компиляции модулей. Откладывание компиляции и загрузки на этап выполнения и разделение этого процесса на множество маленьких частей лишь "оттягивает момент": избежав медленного запуска, связанного с компиляцией всех модулей сразу, мы получаем быстро стартующую систему, которая то и дело "притормаживает".

Данный механизм, по сути, является избыточным, поскольку в PERL уже есть средство динамической компиляции и загрузки модуля – это директива require, автозагрузка просто позволяет распространить этот подход на уровне отдельных функций модуля, что может быть оправданным только в том случае, когда модуль содержит большое количество больших функций. Однако на самом деле такие модули, чаще всего, свидетельствуют лишь о плохом проектировании системы, а вовсе не о её объективной сложности. Таким образом, механизм автозагрузки может рассматриваться как некий "магический" способ делать "заплаты" на разваливающемся проекте быстро и легко. Кроме того, автозагрузка – это одна из причин, по которой PERL не в состоянии обеспечить контроль "достижимости" функций на этапе компиляции, откладывая анализ возможных ошибок на этап выполнения. Таким образом, автозагрузка представляется одним из тех средств, которые, не являясь необходимыми, позволяют системы очень простые и ясные по своей сути делать весьма запутанными по построению. Надо полагать, что такого рода средства и системы, на их основе созданные, есть частное выражение "hubris" (англ. высокомерия – прим. автора) [1].

6.2. Экспорт и импорт

Синтаксисом модуля языка PERL не предусмотрена возможность описания списка экспортируемых объектов. Модуль в PERL может экспортировать лишь переменные и функции, поскольку возможность не то что экспорта, но даже описания констант и типов отсутствует. По умолчанию все переменные и все функции являются экспортируемыми, то есть доступными из внешних модулей, импортирующих данный модуль.

Для объявления частных переменных модуля можно использовать встроенную функцию my, поскольку модуль неявно определяет блок, в котором содержится его тело и которому соответствует своя область видимости. Переменные, объявленные с помощью my, доступны только внутри данного модуля. А вот для описания частных функций модуля соответствующих средств нет – приходится описывать частную переменную-ссылку на анонимную функцию и вызывать её по ссылке:

my $PrivateFunction = sub {
   # тело функции
};

&$PrivateFunction("boo-boo-boo", 24);

Для импорта модулей предназначены две директивы: use и require. Первая используется для статического импорта (на этапе компиляции), а вторая – для динамического (на этапе выполнения). Директива use, кроме имени импортируемого модуля, позволяет указать список импортируемых объектов, например:

use SomeModule ("SomeVariable", "SomeFunction");

Если список указан, и он не пуст, либо если список не указан вовсе, то модуль будет откомпилирован, загружен и будет вызвана функция import, которая может быть определена в импортируемом модуле и обязана самостоятельно реализовать импорт запрашиваемых объектов, имена которых ей будут переданы в качестве параметров. Собственно PERL эту возможность не поддерживает, а если функция import в модуле не определена, то ошибки не возникнет ни на этапе компиляции, ни на этапе выполнения. При этом функция import, фактически, может реализовать не импорт, а любое другое действие, поскольку её реализация отдана на откуп программисту, и PERL не сможет это проконтролировать. Если указан пустой список, то ни один объект не будет импортирован, а функция import вызвана не будет.

Чтобы "облегчить" жизнь разработчику модулей, в стандартную библиотеку PERL включён модуль Exporter, который реализует функцию import с поистине потрясающими возможностями, вот её-то и следует использовать в подавляющем большинстве случаев. Здесь мы не будем подробно останавливаться на этом модуле, интересующихся отошлём к соответствующей man-странице. Заметим лишь, что для того, чтобы воспользоваться всеми "преимуществами" модуля Exporter, ваш модуль должен наследовать модуль Exporter, рассматриваемый в данном случае как класс. Забавно, правда? В PERL понятия и средства модульного и объектно-ориентированного программирования переплетаются таким изощрённым, причудливым образом, что порою можно оказаться в полном недоумении. И это только один из примеров, более подробно ООП будет рассмотрено в следующем разделе.

Надо отметить, что под словом "импорт" в PERL понимается нечто отличное от того, что принято понимать под этим словом в других языках. "Импорт" означает, что при обращении к импортированным объектам можно не использовать квалифицированных идентификаторов, а работать с ними так, будто они описаны в данном модуле. При этом сохраняется возможность обращения к прочим доступным объектам (не my-переменным и всем функциям) с помощью квалифицированных идентификаторов, например так: SomeModule::SomeFunction. Так что ценность такого "импорта" равна нулю: он не позволяет на самом деле управлять видимостью объектов при разработке модуля. А возможность обращения к внешнему объекту по имени без квалификации вредна: на практике всегда следует использовать только квалифицированные идентификаторы при обращении к объектам, импортированным из других модулей. К сожалению, эта простая и ясная идея, способная облегчить читаемость и сопровождаемость кода, не пользуется популярностью среди разработчиков "промышленных" языков программирования, так что она во всей полноте реализована лишь в языке Oberon. К счастью, если не использовать возможности "импорта" в PERL, то программист будет обязан использовать квалифицированные имена при обращении к внешним объектам, в противном случае возникнет ошибка этапа выполнения.

Наконец, отметим, что все имена, определённые в модуле, хранятся в хеше с именем, совпадающим с именем модуля, с "присоединённым" к нему символом "::". Причём этот хеш и его элементы доступны как для чтения, так и для записи. Возможностью "пополнения" таблицы символов модуля активно пользуются такие стандартные модули PERL, как Exporter и overload.

7. Объектно-ориентированное программирование

Хотя в среде PERL-программистов и принято считать, что в языке PERL присутствуют средства объектно-ориентированного программирования (далее ООП), эти средства являются, по сути, надстройкой над средствами модульного программирования, то есть они реализованы на основе уже имеющихся, слабо развитых, как было показано ранее, средств в виде довольно странного их расширения. Так что при использовании PERL программист для реализации объектно-ориентированных проектов вынужден, фактически, обходиться процедурно-ориентированными средствами, поскольку язык PERL не предлагает ровным счётом никаких синтаксических конструкций для поддержки ООП. Ещё бы, ведь с точки зрения разработчиков этого языка, "an object is simply a referenced thingy that happens to know which class it belongs to. A class is simply a package that happens to provide methods to deal with objects. A method is simply a subroutine that expects an object reference (or a package name, for class methods) as its first argument"4 [1]. Таким образом, говорить о наличии средств ООП в языке PERL не приходится, можно говорить только о поддержке тех или иных вспомогательных (не синтаксических) средств, которые при должном их использовании позволяют лишь в той или иной степени реализовать основные концепции, лежащие в основе ООП.

Из приведённой цитаты видно также, что разработчики PERL мало знакомы с природой ООП и лежащей в его основе объектной моделью. Дело в том, что объектная модель "имеет четыре главных элемента: абстрагирование, инкапсуляция, модульность, иерархия. Эти элементы являются главными в том смысле, что без любого из них модель не будет объектно-ориентированной" [3]. Далее, рассмотрев средства, предлагаемые языком PERL для использования при реализации объектно-ориентированных проектов, станет ясно, что средства эти, равно как и результаты, получающиеся при их использовании, далеки от соответствия тем главным элементам объектной модели, о которых было сказано. Кроме того, "понятие класса… служит для того, чтобы дать программисту инструмент для построения новых типов… В идеале использование определённого пользователем типа не должно отличаться от использования встроенных типов. Различия возможны только в способе построения" [12]. Как было показано ранее в разделе "Типы данных, переменные и константы", в языке PERL отсутствует механизм типизации, что делает фактически бессмысленными всякие попытки обеспечить поддержку ООП в нём.

Здесь же сделаем замечание о терминологии. По словам Г. Буча, "унаследовав от многих предшественников, объектный подход, к сожалению, перенял и запутанную терминологию" [3]. С учётом терминологии, использованной в фундаментальных трудах [3] и [12], здесь для краткости и определённости автор будет использовать следующие термины:

класс
для обозначения некоторого множества объектов, имеющих общую структуру и общее поведение
подкласс
для обозначения класса (потомка) по отношению к другому классу (предку), когда класс-потомок наследует от класса-предка своё строение прямо или косвенно; подкласс специализирует суперкласс
суперкласс
для обозначения класса (предка) по отношению к другому классу (потомку), когда класс-предок является классом, от которого класс-потомок наследует своё строение прямо или косвенно; суперкласс является обобщением своих подклассов
объект
для обозначения конкретной сущности, определённую во времени и пространстве; любой конкретный объект является экземпляром класса
метод
для обозначения функций-членов класса; методы определяют поведение объектов
свойство
для обозначения данных-членов класса; свойства определяют состояние объектов

Иногда, где это необходимо, для прочих терминов автор в скобках будет указывать эквивалентные им термины языка C++, выделяя их шрифтом.

7.1. Синтаксис определения класса, область видимости методов и свойств

Ввиду отсутствия языковых средств поддержки ООП, в PERL отсутствуют и средства, специально предназначенные для определения класса. Класс, как уже было замечено в цитате из [1], есть модуль, не больше и не меньше. То есть содержимое модуля определяет члены соответствующего класса. Это означает, что:

  • имя модуля может быть использовано как имя соответствующего класса для создания объектов этого класса;
  • функции, определённые в модуле, могут быть использованы как методы соответствующего класса;
  • глобальные данные, определённые в модуле, могут быть использованы как статические (static) свойства соответствующего класса, разделяемые всеми объектами этого класса (такие данные в PERL называются class data).
  • блоки BEGIN и END выполняют роль конструкторов (class constructor) и деструкторов (class destructor) класса соответственно; их следует отличать от конструкторов и деструкторов объектов (object constructor и object destructor соответственно).

Функции модуля, используемые как методы соответствующего класса, могут быть запрограммированы так, чтобы их можно было использовать как статические (static) методы (такие методы в PERL называются class methods) либо как виртуальные (virtual) методы (такие методы в PERL называются object methods), либо как и те, и другие. В любом случае для обеспечения возможности использования функций модуля в качестве методов соответствующего класса эти функции должны быть запрограммированы исходя из того, что первым параметром (причём, в отличие, скажем, от C++, доступным внутри метода именно как переданный параметр) в них будет передаваться имя класса, экземпляром которого является данный объект, в виде строки (для class methods) либо ссылка на данные конкретного объекта, с которыми этот метод может оперировать (для object methods). Функции, которые предполагается использовать то как class methods, то как object methods в качестве первого параметра будут принимать неизвестно что (то строку, то ссылку), для преодоления этой трудности в PERL предусмотрена встроенная функция ref, которая позволяет выяснить, является ли переданный параметр ссылкой, и для структуры данных, ссылка на которую передана ей в качестве параметра, возвращает:

  • имя соответствующего класса в виде строки;
  • тип ссылки в виде строки ("REF", "SCALAR", "ARRAY", "HASH", "CODE", "GLOB"), если со структурой данных не связано имя класса;
  • пустую строку, если переданный параметр – не ссылка.

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

Поскольку, как уже говорилось в разделе "Модульное программирование", все функции модуля, фактически, являются экспортируемыми, то при использовании соответствующего модуля как класса эти функции становятся публичными (public) методами. При необходимости создать частный (private) метод приходится использовать тот же трюк, который применяется при создании частной (не подлежащей экспорту) функции модуля, то есть объявлять частную переменную-ссылку на анонимную функцию. При этом возможность создания защищённых (protected) методов полностью отсутствует. Последние два обстоятельства могут оказаться весьма неприятными: первое потому, что использование ссылок на анонимные функции неудобно (и медленно) по сравнению с обычными функциями, а второе потому, что на практике, как правило, вместо частных методов класса, доступ к которым возможен только для членов этого класса, требуются защищённые методы, что даёт возможность предоставить доступ классам-потомкам к методам классов-предков, скрыв их при этом от внешнего мира. Впрочем, последней неприятности можно избежать, реализовав отсутствующую в языке возможность самостоятельно, что чуть позже и будет продемонстрировано.

Самым интересным аспектом реализации концепций ООП в языке PERL является то, что представление свойств, то есть собственно данных, лежащих в основе класса, полностью отдано на откуп программисту. Сам по себе язык не предусматривает для этого никаких специальных средств. Как было заявлено в [1], "an object is simply a referenced thingy that happens to know which class it belongs to"5. Таким образом, главное в реализации свойств – это обеспечение динамического размещения данных при создании объектов и передачи ссылки на эти данные при вызове методов. Более конкретно об этом – чуть позже, а сейчас хотелось бы затронуть такой важный вопрос, как инкапсуляция.

Термин инкапсуляция означает "процесс отделения друг от друга элементов объекта, определяющих его устройство и поведение; инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации" [3]. При этом "чаще всего инкапсуляция выполняется посредством скрытия информации, то есть маскировкой всех внутренних деталей, не влияющих на внешнее поведение" [3]. Причём, естественно, "важным преимуществом ограничения доступа является возможность внесения изменений в объект без изменения других объектов" [3]. Однако при реализации классов на PERL невозможно обеспечить ограничение доступа к их свойствам, поскольку, как уже заявил Tom Christiansen в [1], объект класса – это структура данных, к которой можно получить доступ по ссылке. Это означает, что абсолютно все свойства объекта, которые представляют основу его внутренней архитектуры, доступны пользователю этого объекта. Следовательно, забота об обеспечении инкапсуляции (и, таким образом, отчасти сопровождаемости класса) в PERL фактически перекладывается с разработчика класса на разработчиков приложений, использующих соответствующий класс: "while you might know the particular implementation of an object, generally you should treat the object as a black box. All access to the object should be obtained through the published interface via the provided methods. This allows the implementation to be revised, as long as the interface remains frozen (or at least, upward compatible). By published interface, we mean the written documentation describing how to use a particular class. (Perl does not have an explicit interface facility apart from this. You are expected to exercise common sense and common decency.)"6 [1]. Справедливости ради необходимо отметить, что в руководствах по языку PERL (например, в man-странице perltoot) постоянно подчёркивается мысль о том, что "it relies on you to read the documentation of each class"7.

Однако есть один весьма замысловатый способ, с помощью которого можно самостоятельно реализовать отсутствующий в языке PERL механизм ограничения доступа к свойствам объекта. Он основан на нерасторопности (в оригинале: lazy) системы автоматической сборки мусора и представлении объекта не как некоторой структуры данных, а как анонимной функции, которая и предоставляет доступ к свойствам объекта (реализованных, в конечном итоге, в виде структуры данных). Для того, чтобы исключить вызов этой функции вне методов класса (ведь это возможно, поскольку значением объектной переменной класса будет указатель на эту функцию) предлагается использовать встроенную функцию caller, с помощью которой можно узнать имя модуля и функции, из которой был сделан вызов данной функции. Этот способ подробно описан в man-странице perltoot, так что интересующихся им автор отсылает к первоисточнику, здесь же лишь сделаем несколько критических замечаний.

Во-первых, данный способ просто неудобен: вместо прямого доступа к свойствам объекта внутри методов класса приходится вызывать функцию. Во-вторых, использование данного способа ввиду его замысловатости приводит к тому, что классы, разработанные на его основе, с трудом воспринимаются, а значит и хуже сопровождаются. В-третьих, чтобы получить доступ к свойствам объекта внутри метода класса необходимо сделать дополнительный вызов функции, да ещё по ссылке (не учитывая вызова caller и анализа его результата), что заметно "тормозит" доступ к свойствам объекта по сравнению с прямым доступом. Эти недостатки сводят на нет ценность данного способа (автору не удалось обнаружить ни одного примера его использования в стандартной библиотеке PERL, им, судя по всему, не пользуются даже разработчики) и лишний раз подчёркивают неадекватность "средств" поддержки ООП в языке PERL тем требованиям, которые к ним предъявляются объектной моделью.

А как всё это соотносится с объектной моделью, лежащей в основе ООП? Как было только что показано, налицо отсутствие инкапсуляции в том смысле, в каком она понимается в [3] и [12]. Также с трудом можно говорить и об абстрагировании в PERL, поскольку в языках программирования вообще, а в объектно-ориентированных языках в особенности основой для представления абстракций является понятие типа данных вместе с механизмом строгой статической типизации: "без контроля типов само понятие абстракции в языках программирования становится пустым и имеющим чисто академический интерес. Абстракция может работать только в языках, постулирующих строгий статический типовой контроль для каждой переменной и функции… чтобы об объектно-ориентированных языках можно было говорить всерьёз, в них должна быть реализована строгая статическая типизация, которую нельзя было бы нарушить; это дало бы возможность программисту полагаться на компилятор в деле идентификации разного рода несогласованностей" [4]. В языке PERL же, как было показано в разделе "Типы данных, переменные и константы", понятие типа, равно как и механизм статической типизации отсутствуют вовсе. Класс же, с точки зрения автора языка, и вовсе не является типом данных – это лишь модуль.

Кроме того, в языке PERL отсутствует такой важный элемент объектной модели, как модульность. В [3] дано представление о модульности как элементе объектной модели: "Модульность – это свойство системы, которая была разложена на внутренне связные, но слабо связанные между собой модули… Модули выполняют роль физических контейнеров, в которые помещаются определения классов и объектов при логическом проектировании системы… Объект логически определяет границы определённой абстракции, а инкапсуляция и модульность делают их физически незыблемыми… вычленение классов и объектов в проекте и организация модульной структуры – независимые действия… Процесс вычленения классов и объектов составляет часть процесса логического проектирования системы, а деление на модули – этап физического проектирования". Однако нельзя говорить о поддержке языком PERL принципа модульности как самостоятельной концепции в рамках ООП – понятия "класс" и "модуль" здесь смешаны довольно странным образом: "A module is just a reusable package that is defined in a library file whose name is the same as the name of the package (with a .pm on the end). A module may provide a mechanism for exporting some of its symbols into the symbol table of any other package using it. Or it may function as a class definition and make its operations available implicitly through method calls on the class and its objects, without explicitly exporting any symbols. Or it can do a little of both"8 [1].

Таким образом, модуль, с точки зрения объектной модели, – это контейнер для абстракций, определённых как классы (типы данных), а в языке PERL модуль – это контейнер для переменных и функций, которые, в зависимости от способа кодирования, от соглашений, принятых разработчиком модуля и, возможно, оговорённых в документации, и от прочих не имеющих отношения к собственно языку программирования внешних факторов, могут использоваться как модуль программной системы либо как класс, либо как и то, и другое одновременно. В этом проявляется несоответствие понятия модульности в языке PERL и понятия модульности в объектной модели. Кроме того, это несоответствие, в купе с оговорёнными выше, приводит к тому, что, вынужденно поддаваясь тлетворному влиянию "PERL-культуры", программист не только начинает с трудом различать основные концепции программирования, но и вовсе перестаёт правильно понимать их смысл, что может привести к плохому проектированию как процедурно-ориентированных, так и объектно-ориентированных проектов и, как следствие, к огромной массе несопровождаемого кода.

На эти замечания можно возразить, что, мол, есть возможность описать несколько классов в одном файле. В этом случае модуль (package) выполняет роль класса, а файл – роль модуля, физически содержащего определения классов. Однако такого рода возражения не изменяют положения вещей в принципе сразу по двум причинам:

  1. во-первых, понятие "файл" не имеет никакого отношения к понятиям, определяемым в языке программирования (например, понятие "модуль"), поскольку это понятие относится к операционной системе; язык программирования и операционная система – это совершенно разные вещи, и нет никакой объективной необходимости хоть сколько-нибудь их смешивать, равно как нет оснований для смешивания понятий "модуль" и "класс", "язык программирования" и "компилятор с языка программирования" и проч.; принципиально различные понятия во избежание путаницы всегда следует чётко различать;
  2. во-вторых, факт физической расположенности определений классов в одном файле никак не изменяет способа обращения к ним и, таким образом, не даёт возможности на уровне языковых конструкций показать, что эти классы действительно являются частью одного модуля (файла) с определённым именем; следовательно, с точки зрения языка программирования, определение нескольких классов в одном файле равносильно и синтаксически, и семантически определению каждого из них в своём собственном файле:
  3. # Файл SomeModule.pm
    
    package Boo1;
    
    sub New {
       print("Hallo! That's Boo1's constructor!\n");
    }
    
    package Boo2;
    
    sub New {
       print("Hallo! That's Boo2's constructor!\n");
    }
    
    1; # это определённо необходимо, иначе "use" не сработает; чудно, правда?
    
    # Файл Test.pl
    
    use SomeModule; # импортируется файл, а не модуль:
                    # package с именем SomeModule не существует
    use ...         # далее импортируется ещё около 10-15 файлов
    
    SomeModule::Boo1->New(); # can't locate object method "New"
                             # via package "SomeModule::Boo1" at Test.pl at line 6
    Boo2->New(); # Hallo! That's Boo2's constructor! А в каком ты модуле, родной?

О ещё одном основном элементе объектной модели – о наследовании, а также об особенностях его реализации в языке PERL поговорим чуть позже.

7.2. Вызов методов

Как уже говорилось, методы классов в PERL бывают двух видов: class methods (аналог статических функций-членов в C++) и object methods (аналог виртуальных функций-членов в C++). Принципиальное отличие между ними заключается в том, что первые можно вызывать, не создавая при этом объектов класса. Вторые можно вызывать, лишь предварительно создав объект. В виде class methods оформляются, например, методы-конструкторы, которые необходимо вызывать ещё до создания объектов класса. В виде object methods оформляются обычные методы, оперирующие с данными объектов.

Для вызова как class methods, так и object methods следует использовать операцию обращения к элементу данных по ссылке ->, которая для обращения к функциям переопределена так. Если слева от неё указано имя класса (модуля), то при вызове функции, имя которой указано справа, в качестве первого параметра ей неявно будет передано имя класса (модуля) в виде строки, например:

use SomeClass;

$a = SomeClass->SomeMethod();

Несмотря на то, что здесь в вызове функции SomeMethod модуля SomeClass, которая здесь использована как статический метод соответствующего класса, отсутствуют фактические параметры, тем не менее внутри неё значение $_[0] будет равно строке "SomeClass". Это свойство операции -> используется при описании методов-конструкторов (о них далее).

Если же слева от операции -> указано имя переменной-ссылки на некоторую структуру данных, то есть, в терминологии PERL, объект, то при вызове функции, имя которой указано справа, в качестве первого параметра ей неявно будет передана эта ссылка на структуру данных, например:

use SomeClass;

$SomeObject = SomeClass->CreateObject();
$SomeObject->SomeMethod();

Несмотря на то, что здесь в вызове функции SomeMethod, которая здесь использована как object method, отсутствуют фактические параметры, тем не менее внутри неё значение $_[0] будет равно значению переменной SomeObject, что позволит получить доступ к свойствам конкретного объекта.

Наряду с операцией ->, в языке PERL поддерживается и другая форма вызова методов, унаследованная от версии PERL 4. Эта форма называется "indirect object syntax" ("косвенный объектный синтаксис") и годится для вызова как class methods, так и object methods:

use SomeClass;

# Вызов class method,
# он эквивалентен такому вызову:
# $SomeObject = SomeClass->CreateObject("foo");

$SomeObject = CreateObject SomeClass "foo";

# Вызов object method,
# он эквивалентен такому вызову:
# $a = $SomeObject->SomeMethod("boo");

$a = SomeMethod $SomeObject "boo";

Поддержка множества различных способов сделать одно и то же, каждый из которых по-своему уникален теми сюрпризами, которые связаны с его использованием, есть одно из основных свойств языка PERL, ведь общепризнанным девизом программирования на PERL является фраза "there's more than one way to do it"9 [1]. Так, вторая форма вызова методов может быть использована как списковый оператор, когда следующий за ним список параметров можно не заключать в круглые скобки, а первая – нет, поскольку в этом случае список фактических параметров обязательно должен быть заключён в круглые скобки:

# Класс SomeClass
package SomeClass;

sub SomeMethod {
   shift; # удаляем из массива @_ первый элемент – имя класса

   foreach (@_) {
      print($_, " ");
   }

   print("\n");
}

1;

# Программа-клиент

use SomeClass;

SomeClass->SomeMethod  "a", "b", "c", "d";  # ошибка времени выполнения
SomeClass->SomeMethod "a", ("b", "c"), "d"; # ошибка времени выполнения
SomeClass->SomeMethod ("a", "b"), "c", "d"; # будет напечатано: "a b "

SomeMethod SomeClass "a", "b", "c", "d";    # будет напечатано: "a b c d "
SomeMethod SomeClass "a", ("b", "c"), "d";  # будет напечатано: "a b c d "
SomeMethod SomeClass ("a", "b"), "c", "d";  # будет напечатано: "a b "

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

Чтобы интерпретатор имел возможность найти тот класс (модуль), в котором определён вызываемый метод, необходимо предварительно связать объект (структуру данных, на которую указывает ссылка) с именем класса (модуля), экземпляром которого он является. Эта задача обычно решается при создании объекта.

7.3. Создание и удаление объектов

Создание объектов полностью отдано на откуп программисту, в языке PERL отсутствует понятие конструктора, который бы вызывался автоматически при создании объекта. Таким образом, задача выделения памяти для создаваемого объекта и инициализации возлагается на один или несколько обычных методов класса, которые с точки зрения языка ничем не отличаются от остальных методов. Способ создания объектов класса должен оговариваться в документации класса. Обычно метод, который выполняет роль конструктора, называют new, хотя это лишь стилистическое соглашение, а не требование синтаксиса языка.

В языке PERL отсутствуют как средства объявления переменных объектного типа, так и средства динамического создания таких переменных, вроде операции new. Это обусловлено тем, что в PERL отсутствует понятие типа данных вообще и объектного типа в частности, и, как следствие, отсутствуют механизмы как статической, так и динамической типизации. Вместо этого PERL предлагает несколько встроенных функций (о них чуть позже), с помощью которых программист по своему усмотрению может самостоятельно реализовать отсутствующие в языке механизмы, а вся работа по созданию объекта, как уже отмечалось выше, перекладывается на метод-конструктор, вызываемый явно.

Ввиду того, что в PERL отсутствуют средства объявления переменных объектного типа, то есть средства создания статических объектов, все объекты являются динамическими. Именно поэтому "an object is just a referenced thingy"10 [1]. Следовательно, основной задачей метода-конструктора является выделение памяти для создаваемого объекта, то есть его свойств, и возврате ссылки на выделенную и, возможно, инициализированную память. Эта ссылка и будет значением переменной "объектного типа". При этом в качестве основы можно использовать любую структуру данных: скаляр, массив, хеш или произвольную их комбинацию. Вся ответственность за способ внутреннего представления данных объекта и способ его использования полностью возлагается на программиста. Но, естественно, чаще всего для представления данных объекта используется хеш как некий аналог структурного типа, хотя это совершенно необязательно: в man-странице perltoot приведён пример использования для этих целей массива. Вот пример простейшего конструктора:

sub CreateObject {
   return {
      "SomeProperty" => undef,
      "AnotherProperty" => undef,
      "ListOfSomething" => [
         "blah-blah-blah",
         0,
         "boo-boo-boo"
      ]
   }
}

Если данную функцию поместить в класс (модуль) под названием, скажем, SomeClass, то создать объект соответствующего класса можно так:

use SomeClass;

$SomeObject = SomeClass->CreateObject();

или даже так:

use SomeClass;

$SomeObject = SomeClass::CreateObject();

В данном конкретном случае оба способа эквивалентны, что ещё раз подчёркивает неразрывную связь средств поддержки ООП в PERL с средствами модульного программирования.

У созданного таким образом объекта и, соответственно, у конструктора класса есть два больших недостатка. Во-первых, мы в дальнейшем не сможем использовать операцию -> для вызова object methods, например так:

$SomeProperty = $SomeObject->GetSomeProperty();

Дело в том, что интерпретатор не сможет узнать, в каком классе (модуле) определён вызываемый object method, и выдаст сообщение об ошибке времени выполнения: "Can't call method "GetSomeProperty" on unblessed reference at <имя файла> line <номер строки>". Для того, чтобы можно было вызывать object methods с помощью операции ->, необходимо предварительно связать объект с именем класса, экземпляром которого он является.

Во-вторых, создав объект, мы сами потом не сможем узнать, какому классу он принадлежит, то есть узнать "тип" объекта. Ведь в PERL нет операций is и as языка Object Pascal, предназначенных, соответственно, для сравнения и приведения объектных типов. Для анализа объектного типа следует использовать упоминавшуюся уже встроенную функцию ref и метод isa встроенного класса UNIVERSAL, о котором будет сказано далее. Но чтобы эти средства можно было использовать для установления факта принадлежности конкретного объекта определённому классу, необходимо предварительно связать созданный объект с именем класса, экземпляром которого он является.

Для этих целей используется встроенная функция bless, которая принимает два параметра: ссылку на структуру данных и имя класса, с которой её нужно связать. Второй аргумент можно опускать, в этом случае в качестве второго параметра будет использовано имя класса (модуля), в области видимости которого происходит вызов данной функции. Таким образом, приведённый ранее пример метода-конструктора примет следующий вид:

sub CreateObject {
   my $Object = {
      # определяем свойства объекта
      ...
   }

   bless($Object);

   return $Object;
}

Однако и в таком виде конструктор далёк от совершенства, поскольку он не приспособлен для наследования его классами-потомками. Допустим, что этот конструктор определён в классе A, а класс B наследует его от класса A. Тогда, создавая объекты класса A, конструктор свяжет объект с именем "A". Но, создавая объекты класса B, этот же конструктор будет связывать их также с именем "A", а не "B", как это необходимо, поскольку он вызывается в области видимости класса A, и мы не передаём функции bless второй параметр. Таким образом, объекты класса B будут являться, фактически, объектами класса A, что не даст возможности ни вызывать методы, определённые или переопределённые в классе B, ни отличать объекты класса B от объектов класса A.

Для преодоления этой трудности необходимо выполнить три условия:

  1. использовать операцию -> для вызова метода-конструктора;
  2. внутри метода-конструктора из переданного первым параметра извлекать имя класса, объект которого создаётся;
  3. всегда использовать функцию bless с двумя параметрами: в качестве второго параметра необходимо указывать полученное имя класса.

Обычно методы-конструкторы реализуются и используются как class methods, поэтому рассмотрим применение указанных условий для этого случая. Использование операции -> для вызова class methods, как уже говорилось выше, позволяет автоматически передавать в вызываемый метод имя класса, которое можно и нужно использовать в качестве второго параметра функции bless. Таким образом, приведённый ранее пример метода-конструктора примет вид:

sub CreateObject {
   my $Class = $_[0];

   my $Object = {
      # Определяем свойства объекта
      ...
   }

   bless($Object, $Class);

   return $Object;
}

В таком виде конструктор может безболезненно наследоваться, поскольку он будет связывать создаваемый объект с именем именно того класса, экземпляром которого он является, что обеспечит доступ к методам подклассов и правильную идентификацию типа объектов. Более подробно о наследовании поговорим чуть позже.

При желании конструировать новые объекты на основе уже имеющихся метод-конструктор будет использоваться как object method, и в этом случае нужно внутри него выделять из переданной в качестве первого параметра ссылки на объект имя класса, экземпляром которого он является, с помощью встроенной функции ref. Приведём пример такого конструктора:

sub DuplicateObject {
   my $SourceObject = $_[0];
   my $Class = ref($SourceObject);
   my %ObjectCopy = %$SourceObject;
   my $DuplicateObject = \%ObjectCopy;

   bless($DuplicateObject, $Class);

   return $DuplicateObject;
}

Следующий фрагмент исходного кода иллюстрирует использование обоих конструкторов CreateObject и DuplicateObject:

use SomeClass;

# Создаём новый объект
$SomeObject = SomeClass->CreateObject();

# PERL не обеспечивает никакой инкапсуляции...
$SomeObject->{"SomeProperty"} = "coo-coo";

# Конструируем новый объект копированием имеющегося
$AnotherObject = $SomeObject->DuplicateObject();

# Изменяем значение скопированного свойства
$AnotherObject->{"SomeProperty"} = "kaa-kaa";

# Значение этого свойства у первого объекта не изменилось
print($SomeObject->{"SomeProperty"}); # будет напечатано "coo-coo"

Здесь необходимо отметить, что функция bless связывает с именем класса именно структуру данных (объект), на которую указывает переданная ей ссылка, а не саму ссылку, в чём легко убедиться на примере, приведённом в man-странице perlobj:

$a = {};
$b = $a;
bless $a, BLAH; # слово BLAH будет воспринято как строка в кавычках
print "\$b is a ", ref($b), "\n"; # будет напечатано "BLAH", несмотря на то,
                                  # что вызова bless($b, BLAH) не было

Однако, несмотря на это, при копировании объекта в новую область памяти, что и происходит в конструкторе DuplicateObject, его необходимо заново связывать с именем класса, именно поэтому вызов функции bless необходим и в конструкторе DuplicateObject. В противном случае, то есть если вызов функции bless не будет сделан, новый объект не будет "знать", экземпляром какого класса он является:

use SomeClass;

# Создаём новый объект
$SomeObject = SomeClass->CreateObject();

# Конструируем новый объект копированием имеющегося...
$AnotherObject = $SomeObject->DuplicateObject();

# И получаем "безродный" объект
print(ref($AnotherObject); # будет напечатано "HASH"

Вообще, функцию bless можно использовать не только в "конструкторе", но и везде, где вздумается, и так, как вздумается. Связывание объекта некоторого класса с именем другого класса, даже того, который не является суперклассом, причём вне области видимости какого-либо класса, естественно, возможно, и оно никак не контролируется. Смысл этого понять трудно, но это вполне законно. Может быть, таким образом программисту пытались предоставить возможность вручную преобразовывать "объектные типы"? Зачем? Для пущей "гибкости", наверное.

Если параметр, переданный функции ref, не является ссылкой, то будет возвращена пустая строка, что в логическом контексте есть false. Этот факт можно использовать для написания универсального конструктора, который как class method может быть использован для конструирования новых объектов, а как object method может быть использован для копирования существующих объектов. Пример такого совмещения возможностей статического и виртуального метода приведён в man-странице perltoot.

Какой в итоге можно сделать вывод? Средства доступа к методам и свойствам "объектов", также как и средства описания классов и создания объектов, есть просто надстройка над средствами поддержки модульного программирования. В принципе то, что было описано выше, может быть с равным успехом достигнуто и без тех "объектно-ориентированных средств", что предлагает язык PERL, а это, напомню ещё раз, всего пара встроенных функций (bless и ref) и одна операция (->). Связывание имени "класса" с "объектом" можно выполнить и вручную, заранее определив для этого, например, элемент хеша, а вызовы "class methods" и "object methods" можно выполнять как вызовы соответствующих функций соответствующего модуля, передавая нужные параметры явно. Приведём пример:

# Модуль поддержки "средств" ООП
package OOPSupport;

# Аналог встроенной функции ref
sub TypeOf {
   $Object = $_[0];

   return $Object->{"CLASS"};
}

1;

# "Класс", реализованный без встроенных средств ООП
package SomeClass;

# "Конструктор", реализованный как "class method"
sub CreateObject {
   my $Class = $_[0];

   my $Object = {
      # Делаем "bless"
      "CLASS" => $Class;

      # Определяем свойства "объекта"
      "SomePoperty" => "blah-blah-blah"
   }

   return $Object;
}

# "Интерфейсный метод", реализованный как "object method"
sub GetSomeProperty {
   $Object = $_[0];

   return $Object->{"SomeProperty"};
}

1;

# Программа-клиент

use OOPSupport;
use SomeClass;

# Создаём новый "объект", вызываем "class method"
$SomeObject = SomeClass::CreateObject("SomeClass");

# Получаем доступ к "частным" данным, вызываем "class method"
$SomeProperty = SomeClass::GetSomeProperty($SomeObject);

# А это - "анализ типов времени выполнения"
if (OOPSupport::TypeOf($SomeObject) eq "AnotherClass" {
   DoSomething();
} else {
   DoNothing();
}

Приведённый выше фрагмент исходного кода есть наглядный пример объектно-ориентированного программирования на не объектно-ориентированном языке. Однако как мало отличается по своей сути этот код от того, что пишут с использованием "средств поддержки ООП" в PERL, в том числе от приведённых ранее примеров! Особенно это видно по реализации класса, она практически не изменилась! С таким же успехом можно заниматься объектно-ориентированным программированием на языке ассемблера. Единственное, что не учтено в этом примере, так это объектная "ориентированность", то есть возможность наследования. Минутку терпения, скоро мы разберёмся и с этим.

Несмотря на то, что в языке PERL реализован механизм автоматической сборки мусора, он поддерживает деструкторы. Они бывают, как и конструкторы, двух видов: object destructors и class destructors. Object destructor – это метод класса с предопределённым именем DESTROY, который автоматически вызывается интерпретатором PERL на этапе выполнения программы во время утилизации объекта сборщиком мусора. Class destructor – это просто функция с предопределённым именем END соответствующего модуля. По сути то, что в PERL названо словом "деструктор", есть финализатор, поскольку уничтожение объектов, то есть освобождение выделенной для них памяти, происходит автоматически, а финализатор лишь освобождает ресурсы, которые не связаны с собственными ресурсами объекта, например, файловые дескрипторы.

Деструктор можно вызывать и явно, что особенно актуально в связи с тем, что PERL не поддерживает автоматический вызов деструкторов объектов суперклассов при уничтожении объектов подклассов, если в подклассах определены собственные деструкторы, в отличие от языка C++. Это особенно забавно в свете того, что в PERL, в отличие от C++, как конструкторы, так и деструкторы могут наследоваться, поскольку это обычные методы, причём если деструктор не был переопределён, то деструктор суперкласса будет вызван при уничтожении объекта подкласса, в противном случае он должен быть вызван явно, что сильно обесценивает факт поддержки деструкторов, особенно при разработке сложных иерархий классов, тем более здесь нет тех трудностей, что возникают при попытке реализовать возможность автоматического вызова конструкторов суперклассов, которые в общем случае могут принимать параметры. В любом случае поддержка деструкторов при наличии системы автоматической сборки мусора представляется излишеством, особенно учитывая нерасторопность (в оригинале: lazy), свойственную реализации этой системы в языке PERL, и необходимость явного вызова деструкторов в случае наследования: проще явно вызывать соответствующий обычный метод.

7.4. Наследование и полиморфизм

Начнём с цитаты: "Объектно-ориентированое программирование вышло из принципов и понятий традиционного процедурного программирования. Скажу больше: в ООП не добавлено ни одного действительно нового понятия; просто по сравнению с процедурным оно делает значительно более сильный акцент на двух понятиях. Первое – это привязка процедуры к составной переменной… Второе понятие – это конструирование нового типа данных путём расширения заданного типа" [11]. Реализация первого понятия в языке PERL была рассмотрена ранее, теперь рассмотрим "реализацию" второго.

Для управления наследованием, также как и для описания классов, язык PERL не предлагает никаких синтаксических конструкций. Вместо этого для указания классов, от которых наследует данный класс, следует при описании данного класса объявить глобальный массив с предопределённым именем @ISA и включить в него их имена в виде строк. Et vou a la. Этот массив используется при поиске метода, вызванного одним из двух способов, рассмотренных ранее (с помощью операции -> или "indirect object syntax"). При этом, естественно, ни о каком "расширении заданного типа" и речи быть не может: в PERL наследование рассматривается исключительно как возможность автоматического поиска "унаследованных" методов в суперклассах при вызове их для объектов подклассов. Реализация концепции "расширения типа", так же как и конструирование структуры данных, лежащей в основе объектов класса, отдана полностью на откуп программисту. Массив @ISA "обеспечивает" как одиночное, так и множественное "наследование". Рассмотрим их по порядку.

При одиночном наследовании необходимо разрешить три вопроса:

  1. обеспечить правильное конструирование объектов подкласса за счёт явного вызова конструкторов суперклассов;
  2. обеспечить корректное уничтожение объектов подкласса за счёт явного вызова деструкторов суперклассов ввиду того, что возможность автоматического вызова деструкторов реализована в PERL некорректно;
  3. обеспечить доступ как к методам подкласса, так и к методам суперклассов, в том числе при совпадении имён.

Все три вопроса связаны с вызовом методов, и все они разрешаются использованием операции -> или "indirect object syntax". Оба эти способа вызова методов класса, кроме неявной передачи первым параметром имени класса или ссылки на объект, обладают ещё тем свойством, что обеспечивают автоматический поиск нужного метода. В случае одиночного наследования процедура поиска выполняется в такой последовательности:

  1. если метод найден в том классе, для которого (или для объекта которого) он вызван, то будет вызван этот метод;
  2. в противном случае поиск метода будет выполнен в классе, имя которого указано в массиве @ISA, и так далее вверх по иерархии;
  3. если метод всё ещё не найден, что будет вызвана функция AUTOLOAD класса, для которого (или для объекта которого) вызван метод, если она в нём определена;
  4. в противном случае поиск функции AUTOLOAD будет выполнен в классе, имя которого указано в массиве @ISA, и так далее вверх по иерархии;
  5. если метод всё ещё не найден, то будет выдано сообщение об ошибке этапа выполнения.

Из первого пункта этой последовательности видно, что все object methods в языке PERL являются виртуальными. Это и понятно, ведь в PERL нет возможности указать тип объектной переменной при её объявлении. Однако для управления поиском можно при вызове метода указывать класс, с которого необходимо начать процедуру поиска, например:

# Квалифицированные имена методов можно использовать в операции ->
$DerivedClassObject->BaseClass::OverridenMethod(@ParameterList);

# Indirect object syntax также допускает квалифицированные имена
BaseClass::OverridenMethod $DerivedClassObject @ParameterList;

Эта возможность используется как внутри методов класса, так и в клиентских программах как средство вызова метода суперкласса при переопределении его в данном классе: переопределение имеет место при описании в подклассе метода с именем, совпадающим с именем какого-либо метода, описанного в одном из суперклассов. Для обращения к методам ближайшего суперкласса в подклассе вместо явного имени можно использовать предопределённое имя SUPER.

Переопределение методов есть единственное средство поддержки полиморфизма в PERL, такая возможность, как перегрузка методов, доступная, скажем, в языке C++, отсутствует, что, вообще говоря, и к лучшему. Однако это достоинство не есть следствие разумного подхода, использованного при проектировании языка, а, скорее, случайный побочный эффект, вызванный тем обстоятельством, что синтаксис определения методов (функций) не позволяет описать средствами самого языка количество, порядок и типы принимаемых ими параметров, так что методы (функции) различаются только по имени, в отличие, скажем, от языка C++, где кроме имени учитывается ещё и список параметров.

Несмотря на то, что перегрузка методов не реализована в языке PERL, перегрузка операций возможна, причём она реализована с помощью специального модуля overload. Особенностью перегрузки операций в языке PERL является так называемая "автогенерация". Автогенерация – это способность PERL автоматически (читай: неявно) использовать перегруженные операции для выполнения других, но похожих на перегруженные, операций, которые не были перегружены явно. Таким образом PERL автоматически реализует требование о том, что "перегрузив однажды операцию, вы должны перегрузить и все похожие операции" [7]. Так, например, если операция + перегружена, а операции += и ++ – нет, то при вычислении последних неявно будет использована перегруженная операция +. Кроме того, существует возможность перегрузки операций на этапе выполнения с помощью встроенной функции eval, хотя эта возможность есть, по сути, побочный эффект, она не проектировалась специально для использования в таких целях.

Вообще говоря, полезность механизма перегрузки операций весьма сомнительна, не даром от него, как от опасного излишества, отказались при разработке языка Java: "Java не поддерживает перегрузки операций. Данный механизм иногда является источником неоднозначности в программе на C++, и группа разработчиков Java чувствовала, что она вызывает больше неприятностей, чем выгод" [10]. Трудности, связанные с перегрузкой операций, носят творческий характер и кратко могут быть обозначены так: "Операция – это не произвольный значок, который позволяет делать всё, что угодно!.. Вы можете вполне разумно доказывать, что при конкатенации "прибавляете" одну строку к концу другой, поэтому перегрузка + может быть приемлемой для конкатенации… Но вам будет трудно аргументировать, что символы - или * привносят определённый смысл в строку… Лучше полностью избегать перегрузки операций, если есть какая-либо неясность в их значении" [7].

Кроме того, область применения механизма перегрузки операций невелика: "Перегрузка операций была реализована в языке (речь идёт о C++ – прим. автора) прежде всего для того, чтобы вы могли интегрировать разработанный вами арифметический тип в существующую арифметическую систему языка… Этот механизм никогда не предназначался в качестве средства расширения этой системы. Следовательно, перегрузку операций лучше всего применять только в классах, реализующих арифметический тип" [7].

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

В любом случае, перегрузка операций может заметно усложнить сопровождение, поскольку необходимо внимательно следить за тем, какие операции перегружены, а какие нет, какие из операций используются и отключена ли автогенерация. Кроме того, затрудняется отладка, поскольку все ошибки, связанные с перегрузкой операций, могут быть обнаружены только на этапе выполнения. При этом возможны ситуации, когда работает неправильно, не вызывая при этом сообщений об ошибках ни на этапе компиляции, ни на этапе выполнения. Следующие примеры иллюстрируют указанные трудности.

package SomeClass;

use overload
#    Следующшие две строки нужны для выполнения первого примера
#    "+" => \&AddOperator,
#    "=" => \&DuplicateObject;
#    Следующая строка нужна для выполнения второго примера
#    "++" => \&IncOperator;

sub CreateObject {
   my $Class = $_[0];

   my $Object = {
      "Counter" => 0
   };

   bless($Object, $Class);

   return $Object;
}

sub DuplicateObject {
   my $SourceObject = $_[0];
   my $Class = ref($SourceObject);
   my %ObjectCopy = %$SourceObject;
   my $DuplicateObject = \%ObjectCopy;

   bless($DuplicateObject, $Class);

   return $DuplicateObject;
}

sub AddOperator {
   my $Object = $_[0];
   my $Addend = $_[1];

   # Функция AddOperator обладает побочным эффектом:
   # она модифицирует объект, участвующий в вычислении выражения
   $Object->{"Counter"} += $Addend;

   return $Object;
}

sub IncOperator {
   my $Object = $_[0];
   my $Addend = $_[1];

   $Object->{"Counter"}++;

   return $Object;
}

1;

# Первый пример
use SomeClass;

$a = SomeClass->CreateObject();
$b = $a;     # конструктор копии не сработает...
$b = $b + 1; # и будут модифицированы оба объекта $a и $b
print($a->{"Counter"}."\n");
print($b->{"Counter"}."\n");
$b = $a;
$b += 4;     # автогенерация по умолчанию включена
             # и будет вызвана функция SomeClass::AddOperator,
             # а конструктор копии опять не сработал
print($a->{"Counter"}."\n");
print($b->{"Counter"}."\n");

# Второй пример

use SomeClass;

$a = SomeClass->CreateObject();
$a++;        # конструктор копии не определён, но операция ++ сработает
print($a->{"Counter"}."\n");
$b = $a;
$b++;        # ошибка времени выполнения, конструктор копии не определён

Единственным способом решения всех этих трудностей является отказ от автогенерации, явная перегрузка всех необходимых операций, программирование без побочных эффектов. В частности, реализация функции AddOperator должна быть такой:

sub AddOperator {
   my $SourceObject = $_[0];
   my $Addend = $_[1];
   my $DuplicateObject = $SourceObject->DuplicateObject();

   $DuplicateObject->{"Counter"} += $Addend;

   return $DuplicateObject;
}

В языке PERL все классы являются прямыми потомками встроенного класса UNIVERSAL: "You don't see it, but PERL tacitly and irrevocably assumes that there's an extra element at the end of @ISA: the class UNIVERSAL"11 [perltoot]. Этот класс реализует три метода (isa, can и VERSION) и не имеет свойств (конструктор и деструктор отсутствуют). Наиболее важным для программиста является метод isa – он является аналогом операции is языков Object Pascal и Oberon и предназначен для анализа объектных типов на этапе выполнения.

Особенностью класса UNIVERSAL является возможность его непосредственного расширения: несмотря на то, что он встроенный, программист может по своему усмотрению добавить в него методы, которые неявно будут доступны во всех классах. Для этого достаточно включить описания новых методов в файл UNIVERSAL.pm каталога lib. При этом методы isa, can и VERSION являются встроенными и реализованы как часть интерпретатора.

В принципе, наличие в языке класса-предка по умолчанию (вроде UNIVERSAL в PERL или Object в Java) является излишеством, поскольку те возможности, которые в них обычно закладываются, связаны, в основном, с анализом информации о типах на этапе выполнения. Их логичнее было бы реализовать в виде операций, применимых к переменным объектных типов, сделав их, таким образом, частью синтаксиса языка, а не в виде каких-то встроенных классов, да ещё наследуемых неявно: такой подход "поощряет людей смотреть на наследование, как на единственный, или, по меньшей мере, основной метод организации программ, и организовывать классы в иерархии с единственной вершиной" [?12] и приносит больше проблем, чем выгод. Ведь "какое свойство мог бы реализовывать этот общий (класс – прим. автора) object? То есть, какое свойство должен иметь каждый объект каждого класса в вашей программе? Единственное, что приходит на ум, это управление памятью – способность объекта создать самого себя" [7]. Но в PERL, как было уже показано ранее, эта способность специфична для каждого класса, поскольку именно при создании объекта класса и определяются те свойства, которые он реализует, поэтому реализация такой способности в некоем едином для всех классов суперклассе бессмысленна.

Кроме того, "аргументы (функций – прим. автора) класса object практически нельзя использовать непосредственно, потому что сам по себе класс object почти лишён функциональной нагрузки. Вы ловите себя на мысли, что постоянно приводите указатели на object к тому типу, который на самом деле имеет переданный аргумент. Это приведение может быть опасным без использования преобразования типов во время выполнения, потому что вы рискуете сделать преобразование в неверный тип" [7].

Интересна реализация множественного наследования в PERL: "The way it works is actually pretty simple: just put more than one package name in your @ISA array"12 [perltoot]. Несмотря на такую простоту реализации, использование множественного наследования, вообще говоря, может привести к большим трудностям, а в случае PERL – к ещё большим трудностям, чем, скажем, в C++: "It's true that some uses of MI (multiple inheritance – прим. автора) can get you into trouble"13 [perltoot].

Первая проблема связана с вызовом методов: "When it comes time for PERL to go finding methods for your object, it looks at each of these packages (имена которых включены в массиве @ISA – прим. автора) in order… It's actually a fully recursive, depth-first order"14 [perltoot]. Данную особенность следует учитывать при вызове методов классов, наследующих от нескольких суперклассов. К счастью, для управления поиском можно использовать квалифицированные имена так же, как и при одиночном наследовании – их применение здесь особенно уместно.

Другая проблема связана с предопределённым именем SUPER – теперь оно ссылается на тот класс, имя которого указано в массиве @ISA первым, то есть его значение зависит от порядка перечисления базовых классов.

Но самая большая проблема при множественном наследовании связана с конструированием объектов подкласса. Если только базовые классы не являются лишь "объектно-ориентированными" оболочками модулей, реализующими только методы и не имеющими свойств, то при конструировании объекта подкласса необходимо самостоятельно обеспечить:

  • вызов конструкторов суперклассов; в результате будет получено столько новых объектов, сколько суперклассов имеет данный подкласс;
  • копирование свойств полученных объектов в новый объект подкласса – структуру данных, которая будет связана с именем подкласса и ссылка на которую будет возвращена клиенту;
  • контроль совпадающих имён свойств, определённых в различных суперклассах, и механизм разрешения возникающей неоднозначности;
  • механизм адекватного разрешения повторного наследования, которое возникает когда часть суперклассов наследуют от общего предка.

Данные проблемы не могут быть решены на уровне языка PERL, поскольку в нём просто-напросто отсутствуют подходящие для этого средства. В результате все эти проблемы становятся головной болью программиста, что сводит на нет факт "поддержки" множественного наследования в PERL: "Why don't people use MI (multiple inheritance – прим. автора) for object methods much? One reason is that it can have complicated side-effects"15 [perltoot].

Вообще, множественное наследование – "вещь нехитрая, но оно осложняет реализацию языков программирования" [3]. Кроме того, "множественным наследованием часто злоупотребляют… плохо сформированные структуры множественного наследования могут быть сведены к единственному суперклассу плюс агрегация других классов подклассом" [3]. Так что "in practice, few class modules have been seen that actually make use of MI (multiple inheritance – прим. автора). One nearly always chooses simple containership of one class within another over MI"16 [perltoot].

Таким образом, поддержка множественного наследования вряд ли может быть оправданной, поскольку его использование приносит больше проблем, чем выгод: "Множественное наследование несёт с собой несколько частных случаев, которые должны быть обработаны. Оно добавляет накладные расходы как компилятору, так и исполнительной системе, обеспечивая только минимальную выгоду для программиста" [10].

В заключение данного раздела приведём пример объектно-ориентированной программы, написанной без использования средств поддержки ООП в языке PERL. Основное назначение данного примера – продемонстрировать возможность создания такого рода программ и показать, что они ничуть не уступают аналогичным программам, созданным с использованием средств поддержки ООП, предлагаемых PERL, ни с точки зрения производительности, ни с точки зрения надёжности, ни с точки зрения автоматического контроля целостности типов. Возможность разработки такого рода примеров доказывает ущербность реализации концепций ООП в языке PERL, которая не предоставляет программисту никаких реальных преимуществ, кроме, разве что, несколько более краткой записи.

# Модуль управления внешними ресурсами

package ExternalResourceManager;

# Функция доступа к внешнему ресурсу
sub GetExternalResource {
   print("   Getting external resource... ");
   # Нужный код вставьте здесь
   print("done\n");

   return 1;
}

# Функция освобождения выделенного внешнего ресурса
sub FreeExternalResource {
   print("   Destroying external resource... ");
   # Нужный код вставьте здесь
   print("done\n");

   return 1;
}

1;

# Модуль "поддержки" средств ООП

package OOPSupport;

# Эмуляция встроенной функции ref языка PERL
sub TypeOf {
   my $Object = $_[0];

   return $Object->{"CLASS"};
}

# Эмуляция метода UNIVERSAL::isa языка PERL
sub Is {
   my $Object = $_[0];
   my $TargetClass = $_[1];
   my $CurrentClass = $Object->{"CLASS"};
   my $Result = 0;

   print("Analysing ".$CurrentClass."...\n");

   while (defined($CurrentClass) && !$Result) {
      if ($CurrentClass eq $TargetClass) {
         $Result = 1;
      } else {
         $CurrentClass = ${${$CurrentClass."::"}{"SUPER"}};
         print("   Analysing ".$CurrentClass."...\n");
      }
   }

   print("done\n");

   return $Result;
}

# Эмуляция операции as языка ObjectPascal
sub As {
   my $Object = $_[0];
   my $Class = $_[1];
   my %Object = %$Object; # копируем объект в новую область памяти
   my $Result = undef;

   if (Is($Object, $Class)) {
      $Result = \%Object;
      $Result->{"CLASS"} = $Class; # преобразуем тип
      $Result->{"VMT"} = \%{${$Class."::"}{"VMT"}};

      print(
         "Object of class ".$Object->{"CLASS"}.
         " converted to an object of class ".$Class."\n"
      );
   } else {
      print(
         "Runtime error: ".$Object->{"CLASS"}.
         " objects are not type-compatible with ".$Class." objects\n"
      );
   }

   return $Result;
}

# Эмуляция операции -> языка PERL
# Клиенты должны использовать эту и только эту функцию для вызова методов
sub CallObjectMethod {
   my $Object = shift(@_);
   my $Method = shift(@_);
   my $Class = $Object->{"CLASS"};
   my $VMT = $Object->{"VMT"};
   my $Result = undef;

   if (exists($VMT->{$Method})) {
      print("Calling object method ".$Method."...\n");
      $Result = &{$VMT->{$Method}}($Object, @_);
      print("done\n");
   } else {
      print(
         "Runtime error: method ".$Method.
         " doesn't exist in class ".$Class."\n"
      );
   }

   return $Result;
}

# Функция для вызова методов суперкласса
# Эту функцию следует использовать только внутри методов класса
# для вызова методов суперкласса, переопределённых в данном подклассе
# её также можно использовать для вызова любых унаследованных методов,
# чтобы избежать такого кода: &{$VMT{"InheritedMethod"}}($Object, ...)
sub CallSuperclassMethod {
   my $Class = shift(@_);
   my $Method = shift(@_);
   my $Object = shift(@_);
   my %VMT = %{${$Class."::"}{"VMT"}};
   my $Result = undef;

   if (Is($Object, $Class)) {
      if (exists($VMT{$Method})) {
         print("Calling superclass method ".$Method."...\n");
         $Result = &{$VMT{$Method}}($Object, @_);
         print("done\n");
      } else {
         print(
            "Runtime error: method ".$Method.
            " doesn't exist in class ".$Class."\n"
         );
      }
   } else {
      print(
         "Runtime error: ".$Object->{"CLASS"}.
         " objects are not type-compatible with ".$Class." objects\n"
      );
   }

   return $Result;
}

1;

# Базовый класс

package BaseClass;

use ExternalResourceManager;

# Имя ближайшего суперкласса
$SUPER = undef; # ну прямо как в Java

# Таблица виртуальных методов
%VMT = (
   "GetSomeProperty" => \&GetSomeProperty,
   "SetSomeProperty" => \&SetSomeProperty
);

# Для реализации private- и protected-методов можно расширить
# описание %VMT так:
# %VMT = (
#    "SomePublicMethod" => [\&SomePublicMethod, $IsPublic],
#    "SomePrivateMethod" => [\&SomePrivateMethod, $IsPrivate],
#    "SomeProtectedMethod" => [\&SomeProtectedMethod, $IsProtected],
#    ...
# );
# Далее необходимо соответствующим образом модифицировать функции
# CallObjectMethod и CallSuperclassMethod модуля OOPSupport, при этом
# проверку на предмет защищённости вызываемого метода следует включить
# лишь в функцию CallObjectMethod - именно она будет использоваться
# клиентом для вызова методов

# Конструктор
sub CreateObject {
   print("Running BaseClass constructor...\n");

   my $Object = {
      "SomeProperty" => "SomeValue",
      "SomeExternalResource" => ExternalResourceManager::GetExternalResource(),
      "CLASS" => "BaseClass", # это для поддержки "механизма типизации"
      "VMT" => \%VMT # VMT не дублируется в каждом объекте, она одна на класс
   };

   print("done\n");

   return $Object;
}

# Деструктор
sub DestroyObject {
   my $Object = $_[0];

   print("Running BaseClass destructor...\n");

   ExternalResourceManager::FreeExternalResource(
      $Object->{"SomeExternalResource"}
   );

   undef($Object);
   print("done\n");

   return 1;
}

# Интерфейсный метод для чтения
sub GetSomeProperty {
   my $Object = $_[0];

   print("   Running BaseClass::GetSomeProperty\n");

   return $Object->{"SomeProperty"};
}

# Интерфейсный метод для записи
sub SetSomeProperty {
   my $Object = $_[0];
   my $NewValue = $_[1];
   my $PreviousValue = $Object->{"SomeProperty"};

   print("   Running BaseClass::SetSomeProperty\n");
   $Object->{"SomeProperty"} = $NewValue;

   return $PreviousValue;
}

1;

# Производный класс

package DerivedClass;

use OOPSupport;
use BaseClass;
use ExternalResourceManager;

$SUPER = "BaseClass";
%VMT = %BaseClass::VMT; # копируем VMT базового класса

# Переопределяем метод базового класса
$VMT{"SetSomeProperty"} = \&SetSomeProperty;

# "Расширяем" базовый класс
$VMT{"GetOtherProperty"} = \&GetOtherProperty;
$VMT{"SetOtherProperty"} = \&SetOtherProperty;

sub CreateObject {
   print("Running DerivedClass constructor...\n");

   # Вызываем конструктор базового класса
   # Условимся в каждом классе явно определять конструкторы и деструкторы,
   # а также явно вызывать конструктор и деструктор ближайшего суперкласса
   # Такой подход одновременно прост (конструкторы и деструкторы будут
   # вызваны по цепочке для всех суперклассов иерархии) и эффективен (не
   # нужно использовать наследование через VMT и медленный вызов
   # OOPSupport::CallSuperclassMethod)
   my $Object = BaseClass::CreateObject();

   # "Расширяем" базовый класс
   $Object->{"OtherProperty"} = 0;
   $Object->{"OtherExternalResource"} =
      ExternalResourceManager::GetExternalResource();

   $Object->{"CLASS"} = "DerivedClass"; # не будем забывать о
                                        # "механизме типизации"...
   $Object->{"VMT"} = \%VMT; # и собственной VMT, конечно
   print("done\n");

   return $Object;
}

sub DestroyObject {
   my $Object = $_[0];

   print("Running DerivedClass destructor...\n");

   # Деструкторы, как и положено, вызываются в обратном порядке
   ExternalResourceManager::FreeExternalResource(
      $Object->{"OtherExternalResource"}
   );

   BaseClass::DestroyObject($Object);
   print("done\n");

   return 1;
}

# Переопределённый метод
sub SetSomeProperty {
   my $Object = $_[0];
   my $NewValue = $_[1];

   print("   Running DerivedClass::SetSomeProperty\n");

   # Вызов метода суперкласса
   # Здесь можно было бы использовать явный вызов:
   # BaseClass::SetSomeProperty(@_), но этот подход недостаточно гибок,
   # поскольку он не работает в случае, если данный метод реализован не
   # в непосредственном суперклассе, а в более раннем предке
   my $PreviousValue = OOPSupport::CallSuperclassMethod(
      "BaseClass", "SetSomeProperty", @_
   );

   $Object->{"SomeProperty"} .= " (c) Fictivity Inc.";

   return $PreviousValue;
}

sub GetOtherProperty {
   my $Object = $_[0];

   print("   Running DerivedClass::GetOtherProperty\n");

   return $Object->{"OtherProperty"};
}

sub SetOtherProperty {
   my $Object = $_[0];
   my $NewValue = $_[1];
   my $PreviousValue = $Object->{"OtherProperty"};

   print("   Running DerivedClass::SetOtherProperty\n");
   $Object->{"OtherProperty"} = $NewValue;

   return $PreviousValue;
}

1;

# ещё один производный класс

package OtherDerivedClass;

use DerivedClass;

$SUPER = "DerivedClass";
%VMT = %DerivedClass::VMT;

sub CreateObject {
   print("Running OtherDerivedClass constructor...\n");

   my $Object = DerivedClass::CreateObject();

   $Object->{"CLASS"} = "OtherDerivedClass";
   $Object->{"VMT"} = \%VMT;
   print("done\n");

   return $Object;
}

sub DestroyObject {
   my $Object = $_[0];

   print("Running OtherDerivedClass destructor...\n");
   DerivedClass::DestroyObject($Object);
   print("done\n");

   return 1;
}

1;

# Программа-клиент

use OOPSupport;
use OtherDerivedClass;

$OtherDerivedClassObject = OtherDerivedClass::CreateObject();

print(
   "OtherDerivedClassObject SomeProperty is: ".
   OOPSupport::CallObjectMethod(
      $OtherDerivedClassObject, "GetSomeProperty"
   )."\n"
);

print(
   "OtherDerivedClassObject OtherProperty is: ".
   OOPSupport::CallObjectMethod(
      $OtherDerivedClassObject, "GetOtherProperty"
   )."\n"
);

OOPSupport::CallObjectMethod(
   $OtherDerivedClassObject, "SetSomeProperty", "SomeOtherValue"
);

OOPSupport::CallObjectMethod(
   $OtherDerivedClassObject, "SetOtherProperty", 1
);

print(
   "OtherDerivedClassObject SomeProperty is: ".
   OOPSupport::CallObjectMethod(
      $OtherDerivedClassObject, "GetSomeProperty"
   )."\n"
);

print(
   "OtherDerivedClassObject OtherProperty is: ".
   OOPSupport::CallObjectMethod(
      $OtherDerivedClassObject, "GetOtherProperty"
   )."\n"
);

$ClassName = "BaseClass";

if (OOPSupport::TypeOf($OtherDerivedClassObject) eq $ClassName) {
   print("OtherDerivedClass objects are of type ".$ClassName."\n");
} else {
   print("OtherDerivedClass objects are not of type ".$ClassName."\n");
}

if (OOPSupport::Is($OtherDerivedClassObject, $ClassName)) {
   print(
      "OtherDerivedClass objects are type-compatible ".
      "with ".$ClassName." objects\n"
   );
} else {
   print(
      "OtherDerivedClass objects are not type-compatible ".
      "with ".$ClassName." objects\n"
   );
}

$BaseClassObject = OOPSupport::As($OtherDerivedClassObject, "BaseClass");

OOPSupport::CallObjectMethod(
   $OtherDerivedClassObject, "SetOtherProperty", 2
); # ok

OOPSupport::CallObjectMethod(
   $BaseClassObject, "SetOtherProperty", 2
); # oops!

OtherDerivedClass::DestroyObject($OtherDerivedClassObject);
OtherDerivedClass::DestroyObject($BaseClassObject);

# Ну чем вам не C++?

Вывод этой программы представлен ниже.

Running OtherDerivedClass constructor...
Running DerivedClass constructor...
Running BaseClass constructor...
   Getting external resource... done
done
   Getting external resource... done
done
done
Calling object method GetSomeProperty...
   Running BaseClass::GetSomeProperty
done
OtherDerivedClassObject SomeProperty is: SomeValue
Calling object method GetOtherProperty...
   Running DerivedClass::GetOtherProperty
done
OtherDerivedClassObject OtherProperty is: 0
Calling object method SetSomeProperty...
   Running DerivedClass::SetSomeProperty
Analysing OtherDerivedClass...
   Analysing DerivedClass...
   Analysing BaseClass...
done
Calling superclass method SetSomeProperty...
   Running BaseClass::SetSomeProperty
done
done
Calling object method SetOtherProperty...
   Running DerivedClass::SetOtherProperty
done
Calling object method GetSomeProperty...
   Running BaseClass::GetSomeProperty
done
OtherDerivedClassObject SomeProperty is: SomeOtherValue (c) Fictivity Inc.
Calling object method GetOtherProperty...
   Running DerivedClass::GetOtherProperty
done
OtherDerivedClassObject OtherProperty is: 1
OtherDerivedClass objects are not of type BaseClass
Analysing OtherDerivedClass...
   Analysing DerivedClass...
   Analysing BaseClass...
done
OtherDerivedClass objects are type-compatible with BaseClass objects
Analysing OtherDerivedClass...
   Analysing DerivedClass...
   Analysing BaseClass...
done
Object of class OtherDerivedClass converted to an object of class BaseClass
Calling object method SetOtherProperty...
   Running DerivedClass::SetOtherProperty
done
Runtime error: method SetOtherProperty doesn't exist in class BaseClass
Running OtherDerivedClass destructor...
Running DerivedClass destructor...
   Destroying external resource... done
Running BaseClass destructor...
   Destroying external resource... done
done
done
done
Running OtherDerivedClass destructor...
Running DerivedClass destructor...
   Destroying external resource... done
Running BaseClass destructor...
   Destroying external resource... done
done
done
done

Заключение

А что же можно сказать в заключение? Апологеты PERL, наверное, скрежещут зубами за ту горсть горьких пилюль, что пришлось только что проглотить. Они-то знают, что многих мелких неприятностей их тех, что обсуждались выше, при программировании на PERL можно избежать, указывая директивы use strict и use diagnostics в самом начале каждого файла и запуская интерпретатор с опцией -w, что для борьбы с неэффективностью, связанной с необходимостью компиляции программы целиком при каждом запуске, существует целый ряд стандартных модулей, вроде AutoLoader или SelfLoader, а для повышения "прозрачности" при разработке "классов" существуют дополнительные модули, доступные через CPAN. Жаль, что большинство из них не в состоянии понять, что дело тут не в опциях и директивах или прочей дребедени, не имеющей отношения к языку программирования, а в собственно языке программирования, тем более что никакие директивы, никакие опции и никакие дополнительные модули, по большому счёту, не спасают от самых главных недостатков PERL, имеющих принципиальное значение: отсутствия механизма статической типизации, избыточности языковых средств, отсутствия модульности и извращённой реализации ООП. Как говорится, "есть вещи, которые надо продумывать до, а не после" [11].

Автор хотел бы извиниться за порою весьма резкий тон и лёгкий сарказм, однако уникальным свойством языка PERL является то, что критиковать его можно практически бесконечно, – это один из тех языков, в котором всё сделано не так, как нужно. В данной статье автор постарался сконцентрироваться на самых основных элементах языка, имеющих принципиальное значение, и рассмотреть их как можно тщательнее. Автор надеется, что ему это удалось. К сожалению, за рамками статьи остались такие важные вопросы, как ввод-вывод и концепция файлов, синтаксис регулярных выражений и средства форматирования текстовых данных, стандартная библиотека PERL, а также некоторые другие. Анализ каждого из них, возможно, потребовал бы написания отдельных статей. Тем не менее, представленный здесь материал обширен достаточно для того, чтобы можно было сделать вполне обоснованные выводы.

После всего сказанного можно отметить, что язык PERL является крайне плохо структурированным, в принципе нетипизированным и, как результат, весьма ненадёжным языком программирования с избыточным, неясно определённым синтаксисом. Вынужденное использование неэффективных как с точки зрения объёмов памяти, так и с точки зрения производительности динамических структур данных, навязываемых данным языком, а также отсутствие механизма статической типизации приводит к тому, что программы, написанные на PERL, крайне неэффективны, что ещё более усугубляется необходимостью компиляции программы целиком, включая все используемые ею модули, при каждом запуске. Кроме того, большая часть работ по отладке программ, написанных на PERL, может быть выполнена в принципе только на этапе выполнения, что страшно затрудняет и резко замедляет не только собственно процесс отладки, но также и многократно усложняет процесс сопровождения.

Средства, предлагаемые языком PERL для решения даже рядовых задач программирования, зачастую уродливы и опасны сюрпризами и побочными эффектами, они разрушают чёткое, структурированное мышление, никак не способствуют повышению дисциплины при программировании, а также стимулируют небрежное проектирование. Регулярное практическое применение данного языка формирует неверное понимание основных концепций, которые выкристаллизовались в ходе развития технологии программирования.

В связи с этим можно только сожалеть о том, что язык PERL получил такое широкое распространение, в том числе как одно из основных средств разработки Internet-проектов. Причиной тому, очевидно, является фактическое отсутствие альтернативы, которая была бы такой же распространённой, переносимой и, в то же время, бесплатной. Язык Java, несмотря все его достоинства именно как гораздо более надёжного, концептуально выдержанного языка программирования, здесь явно проигрывает, поскольку является, в отличие от доступного в исходных текстах PERL, фирменной и, наверное, недешёвой разработкой Sun Microsystems, Inc. Что ж, в данной ситуации остаётся только надеяться, что, возможно, со временем здесь всё же произойдут положительные изменения – прогресс не стоит на месте, особенно в такой динамичной области, как программирование.

В заключение стоит процитировать слова Н. Вирта: "Да, я убеждён, что есть нужда в высококачественном ПО. И придёт время, когда будет признано, что стоит вкладывать усилия в его разработку и в использование точного и структурированного подхода на основе безопасных, структурированных языков" [11].

Литература

  1. Wall L., Christiansen T., Schwartz R. Programmig PERL. – O'Reilly & Associates, 1996.
  2. Бейбер Р. Л. Программное обеспечение без ошибок. – М.: Радио и связь, 1996.
  3. Буч Г. Объектно-ориентированное программирование. – М.: Конкорд, 1992.
  4. Вирт Н. Долой "жирные" программы //Открытые системы. – 1996. – № 6. – с. 26-31.
  5. Вирт Н. Оценка языка программирования Pascal //Языки программирования Ada, C, Pascal. Сравнение и оценка. – М.: Радио и связь, 1989. – с. 145-162.
  6. Выгодский М. Я. Справочник по элементарной математике. – М.: Физматгиз, 1962.
  7. Голуб А. C и C++. Правила программирования. – М.: Бином, 1996.
  8. Грэхем Р., Кнут Д., Паташник О. Конкретная математика. Основание информатики. – М.: Мир, 1998.
  9. Лекарм О., Дежарден П. Дополнительные замечания по поводу языка программирования Pascal //Языки программирования Ada, C, Pascal. Сравнение и оценка. – М.: Радио и связь, 1989. – с. 219-235.
  10. Ноутон П., Шилдт Г. Java 2. – СПб.: БХВ-Петербург, 2000.
  11. Пешио К. Никлаус Вирт о культуре разработки ПО //Открытые системы. – 1998. – № 1. – с. 41-44.
  12. Страуструп Б. Язык программирования C++. – М.: Мир, 1992.

Владимир, октябрь-декабрь 2001 г.

1 Тремя главными добродетелями программиста являются лень, нетерпение и высокомерие.

2 В высшей степени загадочна.

3 Язык PERL во многом является простым языком.

4 Объект – это просто структура данных (англ. разг. “thingy” означает буквально «штуковина» – прим. автора), которая была связана с именем некоторого класса и к которой можно получить доступ по ссылке. Класс – это просто модуль, экспортирующий методы для обработки объектов. Метод – это просто подпрограмма, принимающая в качестве первого параметра ссылку на объект (или имя класса, если это статический метод).

5 Объект – это просто структура данных (англ. разг. “thingy” означает буквально «штуковина» – прим. автора), которая была связана с именем некоторого класса и к которой можно получить доступ по ссылке.

6 Несмотря на то, что, как правило, внутренняя реализация объекта известна, следует рассматривать объект как «чёрный ящик». Это означает, что любой доступ к объекту следует осуществлять только с помощью документированного интерфейса, реализованного в виде методов. Такой подход позволяет изменить внутреннюю реализацию объекта, при этом интерфейс оставив неизменным (или, по крайней мере, совместимым с предыдущей версией). Под документированным понимается такой интерфейс, который описан в спецификации класса, определяющей порядок его использования. В дополнение к этому язык PERL не обеспечивает никаких возможностей для явного описания интерфейса. Это означает, что от пользователя ожидают благоразумия и порядочности при использовании класса.

7 Обязанностью пользователя является изучение спецификации каждого используемого им класса.

8 Модуль – это пакет повторно используемых подпрограмм, описанный в библиотечном файле с именем, совпадающим с именем пакета (и с суффиксом .pm на конце). В одном случае модуль может обеспечивать механизм экспорта некоторых из определённых в нём имён в таблицу имён любого использующего его модуля. В другом случае модуль может вести себя как класс, когда имена не экспортируются явно, а определённые в нём подпрограммы используются как классовые или объектные методы. Возможен случай, когда модуль реализует и то, и другое.

9 Есть не один способ сделать это.

10 Объект – это просто структура данных (англ. разг. “thingy” означает буквально «штуковина» – прим. автора), к которой можно получить доступ по ссылке.

11 В языке PERL принято, что имя класса UNIVERSAL неявно включено в массив @ISA последним элементом.

12 Способ реализации, на самом деле, довольно прост: достаточно включить в свой массив @ISA не одно, а несколько имён классов.

13 Правда, использование множественного наследования порой может быть связано с трудностями.

14 Когда вызывается метод объекта, PERL выполняет поиск этого метода в классах в порядке включения их имён в массив @ISA… На самом деле это рекурсивный поиск в глубину.

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

16 На практике лишь немногие классы на самом деле используют множественное наследование. Вместо него практически всегда предпочитают простое агрегирование одного класса другим.

11.02.2012 13:22:38 SPQR (IP) Цитата #21
Не обязательно зависимость. Например, каким образом привлекать к разработке проекта новых людей? Каким образом поддерживать свободный проект, к которому первоначальные разработчики потеряли интерес?
25.09.2012 15:18:31 Андрей (IP) Цитата #22
Похоже у автора статьи очень много свободного времени. Да, язык Perl не тривиален. Пишу на нем около 10 лет и до сих пор нахожу какие-то новые возможности или подземные камни. Но, как по мне это можно отнести к достоинствам языка, а не к недостаткам. Это все равно, что критиковать 12 струнную гитару потому что она очень заумная и на ней трудно играть, и предлагать вместо нее играть на балалайке. Все сетования автора, на самом деле, от слабого понимания идиологии языка и недостаточного опыта работы с языком. Не думайте, что вы умнее всего мирового Perl сообщества.
26.09.2012 12:39:39 SPQR (IP) Цитата #23
Если идеологию не понять и за 10 лет, так может ну её?
08.08.2013 17:33:58 SPQR (IP) Цитата #24
Perl, the first postmodern computer language
09.07.2014 18:00:16 Keeper (IP) Цитата #25
Статья интересная, но неграмотная. Сравнивать в одном абзаце посимвольные операции с регэкспами – это на уровне «теплое с мягким». Про выразительность кода автор, похоже, вообще не слышал. Ну и дальше неточностей хватает.
29.11.2014 00:37:08 jlblc (IP) Цитата #26
Очередной раз убеждаюсь, что трудолюбие и интеллект у одного человека величины обратно пропорциональные, вон сколько накропал про нелюбимый перл.
29.11.2014 08:05:21 Станислав (IP) Цитата #27
А замечания по существу написанного автором у Вас есть? Или Вы, «блистая интеллектом» и ленью, предпочитаете вот так кратко и не про статью, а про личность автора?..
30.08.2015 06:52:13 Artefact (IP) Цитата #28
Иван – преподаватель. Поэтому большая часть обличений сводится к несоответствию Perl каким-либо концепциям, а не к его практическим недостаткам, хотя автор разумеется, мои оспорит, ибо с его точки зрения – это и есть практические недостатки :-)
08.06.2016 10:44:24 Keeper (IP) Цитата #29
И даже так можно! http://www.catonmat.net/blog/secret-perl-operators/#goatse
Добавьте свой комментарий или войдите, чтобы подписаться/отписаться.
Имя: OpenId
Результат операции:
Предпросмотр Улыбка Подмигивание Дразнит Оскал Смех Огорчение Сильное огорчение Шок Сумасшествие Равнодушие Молчание Крутизна Злость Бешенство Смущение Сожаление Влюблённость Ангел Вопрос Восклицание Жирный Курсив Подчёркивание Зачёркивание Размер шрифта Гиперссылка Цитата
Загрузка…