Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

9.5. Программы, которые пишут программы

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

Один обычный пример дает динамическая генерация HTML для web-страниц. HTML — это язык, хоть и достаточно ограниченный; кроме того, в себе он может содержать и код JavaScript. Web-страницы часто генерируются "на лету" программами на Perl или С, содержание таких страниц (например, результаты поиска или реклама, нацеленная на опре­деленную аудиторию) зависит от приходящих запросов. Мы использо­вали специализированные языки для графиков, картинок, таблиц, мате­матических выражений и индекса этой книги. Еще одним примером может служить PostScript — язык программирования, тексты на кото­ром создаются текстовыми процессорами, программами рисования и мно­жеством других источников; на финальном этапе обработки книга, кото­рую вы держите в руках, представлена как программа на PostScript, содержащая 57 000 строк.

Документ — это статическая программа, однако идея использования языка программирования как способа записи любой проблемы является весьма многообещающей. Много лет назад программисты мечтали о том, что компьютеры когда-нибудь смогут сами писать для себя программы. Конечно же, эта мечта никогда нe осуществится в полной мере, однако сегодня можно сказать, что машины нередко пишут программы за нас и иногда в таких областях, где совсем недавно это трудно было себе пред­ставить.

Наиболее распространенные программы, создающие программы, — это компиляторы, которые переводят программу с языка высокого уров­ня в машинный код. Однако нередко оказывается удобным переводить код программы сначала на один из широко известных языков высокого уровня. В предыдущем параграфе мы упоминали о том, что генератор синтаксического анализатора преобразует определение грамматики языка в программу на С, которая и занимается синтаксическим разбо­ром языка. Язык С достаточно часто используется подобным образом — в качестве "языка ассемблера высокого уровня". Modula-3 и C++ отно­сятся к тем языкам общего назначения, для которых первые компилято­ры создавали код на С, обрабатывавшийся затем уже стандартным ком­пилятором. У такого подхода есть ряд преимуществ — одним из главных является эффективность, поскольку получается, что в принципе про­грамма может выполняться так же быстро, как и программы на С. Еще один плюс — переносимость: такие компиляторы могут быть перенесе­ны на любую систему, имеющую компилятор С. Подобный подход силь­но помог этим языкам на ранних стадиях их внедрения.

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

Несмотря на мощь программ-генераторов и на большое количество примеров их удачного применения, этот подход еще не признан так ши­роко, как он того заслуживает, и нечасто используется программистами. Однако существует уйма простых возможностей программного созда­ния кода, и вы вполне можете использовать их преимущества. Ниже приведены несколько примеров генерации кода на С или C++.

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

/* errors.h: стандартные сообщения об ошибках */

enum {

Eperm, /* Доступ запрещен */

Еio, /* Ошибка ввода/вывода */

Efile, /* Файл не существует */

Eniem, /* Переполнение памяти */

Espace, /* Нет места для файла */

Egreg /* Гришина ошибка */

};

Имея такой фрагмент на входе, несложная программа сможет произвес­ти следующий набор деклараций для сообщений об ошибках:

/* machine-generated; do not edit. */

char *errs[] = {

"Доступ запрещен", /* Брега */

"Ошибка ввода/вывода", /* Еio */

"Файл не существует", /* Efile */

"Переполнение памяти", /* Emem */

"Нет места для файла", /* Espace */

"Гришина ошибка", /* Egreg */

};

У такого подхода есть несколько достоинств. Во-первых, соотношение между значениями enum и строками, которые они представляют, получает­ся самодокументированным, и его нетрудно сделать независимым от род­ного языка пользователя. Во-вторых, информация хранится только в од­ном месте, в одном "истинном месте", из которого генерируется весь остальной код, так что и все обновление информации выполняется лишь в одном месте. В случае, когда таких мест несколько, после ряда обновле­ний они неизбежно начнут друг другу противоречить. И наконец, нетруд­но сделать так, чтобы файлы программ на С создавались заново и пере­компилировались при каждом изменении заголовочного файла. Когда потребуется изменить сообщение об ошибке, все, что надо будет сде­лать, — это изменить заголовочный файл и компилировать таким спосо­бом операционную систему, и тогда сообщения автоматически обновятся.

Программа-генератор может быть написана на любом языке. Особен­но просто это сделать на языке, специально предназначенном для обра­ботки строк, таком как Perl:

# enum.pl: генерирует строки сообщений по enum + комментарии

print "/* machine-generated; do not edit. */\n\n";

print "char *errs[] = {\n";

while (<>) {

chop; # удалить перевод строки

if (/^\s*(E[a-z0-9]+),?/) { # первое слово - E...

$name = $1; # сохранить имя

s/.*\/\* *//; # удалить до /*

s/ *\*\///; # удалить */

print "\t\"$_\", /* $name */\n";

}

}

print "};\n";

Опять в дело пошли регулярные выражения. Выбираются строки, у ко­торых первое поле выглядит как идентификатор, после которого стоит запятая. Первая замена удаляет все символы до первого непустого сим­вола строки комментария, а вторая — символы конца комментария и все предшествующие им пробелы.

Среди прочих способов для тестирования компилятора Энди Кёниг (Andy Koenig) разработал метод написания кода C++, позволяющий проверить, нашел ли компилятор ошибки в программе. Фрагменты кода, которые должны вызвать реакцию компилятора, снабжаются специаль­ными комментариями, в которых описываются ожидаемые сообщения. Каждая строка такого комментария начинается с /// (чтобы их можно было отличить от обычных комментариев) и регулярного выражения, которое должно соответствовать диагностике компилятора, выдаваемой для этой строки. Таким образом, например, следующие два фрагмента кода должны вызвать реакцию компилятора:

int f() {}

/// warning.* non-void function .* should return a value

void g() {return 1;}

/// error.* void function may not return a value

Если мы пропустим второй тест через компилятор C++, то он напечатает ожидаемое сообщение, вполне соответствующее регулярному выражению:

% СС х.с

"х.с", line 1: error(321): void function may not return a value

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

Идея использования семантических комментариев не нова. Такие ком­ментарии используются в языке PostScript, где они начинаются с сим­вола %. Комментарии, начинающиеся с %%, могут содержать дополни­тельную информацию о номерах страниц, окаймляющем прямоугольнике (Bounding Box), именах шрифтов и т. п.:

%%PageBoundingBox: 126 307 492 768 %%

Pages: 14

%%DocumentFonts: Helvetica Times-Italic Times-Roman

LucidaSans-Typewrite г

В языке Java комментарии, которые начинаются с /* * и заканчиваются */, используются для создания документации для следующего за ними опи­сания класса. Глобальным вариантом самодокументации кода является так называемое грамотное программирование (literate programming), при котором программа и ее документация интегрируются в один доку­мент, и при одной обработке документ готовится для чтения, а при дру­гой программа готовится к компиляции.

Во всех рассмотренных выше примерах важно отметить роль нота­ции, смешения языков и использования инструментов. Их комбиниро­вание усиливает и подчеркивает мощь отдельных компонентов.

Упражнение 9-15

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]