Искусство метапрограммирования, Часть 1: Введение в метапрограммированиеИсточник: IBM developerWorks Россия Джонатан Бартлет
Оглавление
Одной из самых неиспользуемых технологий программирования является написание программ, генерирующих программы или части программ. Узнайте, почему метапрограммирование необходимо, и рассмотрите некоторые его компоненты (текстовые макроязыки, специализированные генераторы кода). Узнайте, как создать генератор кода, и познакомьтесь ближе с чувствительным к языку макропрограммированием в Scheme. Генерирующие код программы часто называют метапрограммами ; написание этих программ называется метапрограммированием. Создание программ, генерирующих код, имеет многочисленные применения. Данная статья объясняет, почему вам желательно знать метапрограммирование, и рассматривает некоторые компоненты этого искусства - мы детально рассмотрим текстовые макроязыки, исследуем специализированные генераторы кода и обсудим, как их создать, проанализируем чувствительное к языку макропрограммирование с использованием Scheme. Различные применения метапрограммированияВо-первых, вы можете писать программы, которые будут предварительно генерировать таблицы данных для использования во время исполнения программы. Например, если вы создаете игру и хотите выполнять быстрый поиск в таблице синусов для всех 8-битных целых чисел, то можете либо вычислять каждый синус и закодировать это таким образом, чтобы программа строила таблицу при запуске, либо написать программу для создания специализированного кода для таблицы перед компиляцией. Хотя для такого маленького набора чисел может иметь смысл создание таблицы во время исполнения, другие подобные задачи могут чрезмерно замедлить время запуска программы. В таких случаях написание программы построения статических данных обычно является наилучшим вариантом. Во-вторых, если у вас есть большое приложение, в котором множество функций включает длинный стереотипный код, вы можете создать мини-язык, который будет создавать стереотипный код вместо вас и даст вам возможность кодировать только важные части программы. Здесь, если вы сможете, лучше всего абстрагировать стереотипные фрагменты в функцию. Но часто эти фрагменты не столь приятны. Возможно есть список переменных, которые нужно объявить в каждом экземпляре, возможно есть необходимость зарегистрировать обработчики ошибок, возможно существует несколько стереотипных фрагментов, которые должны включать код в определенных обстоятельствах. Все это делает создание простой функции невозможным. Часто в таких ситуациях хорошей идеей является создание мини-языка, что позволит вам работать с таким кодом более простым способом. Этот мини-язык затем конвертируется в исходный код на обычном языке программирования перед компиляцией. Наконец, многие языки программирования вынуждают вас писать многословные операторы для выполнения простых задач. Генерирующие код программы позволяют вам сократить эти операторы и сохранить много времени, расходуемого на набор кода, что также предохраняет от многих ошибок из-за уменьшения шанса возникновения опечаток. По мере приобретения языком дополнительных возможностей, генерирующие код программы становятся менее привлекательными. То, что является доступным как стандартная возможность в одном языке, может быть доступно в другом только при использовании генерирующей код программы. Однако, неадекватный дизайн языка является не единственной причиной необходимости в программах, генерирующих код. Важную роль имеет еще и более легкое обслуживание. Основные текстовые макроязыкиГенерирующие код программы дают вам возможность разрабатывать и использовать маленькие, специфические для конкретной области языки, на которых легче писать и поддерживать программы, чем писать их на целевом языке. Средства для создания этих предметных языков обычно называются макроязыками. В этой статье рассматриваются некоторые типы макроязыков, а также обсуждается, как вы можете использовать их для улучшения вашего кода. Препроцессор C (CPP)Сначала давайте посмотрим на метапрограммирование, в котором используются текстовые макроязыки. Текстовым макросом является макрос, который непосредственно влияет на текст, написанный на языке программирования, и при этом не знает языка или не имеет отношения к его смыслу. Наиболее широко используемыми текстовыми макросистемами являются препроцессор C и макропроцессор M4. Если вы работали с языком C, то, возможно, имели дело с макросом Листинг 1. Простой макрос для перестановки двух значений
Этот макрос дает вам возможность переставить два значения данного типа. Эту операцию лучше всего делать в макросе по нескольким причинам:
В листинге 2 приведен пример используемого макроса: Листинг 2. Использование макроса SWAP:
Препроцессор C во время исполнения дословно изменяет текст Текстовая подстановка - это полезная, но довольно ограниченная возможность. С ней есть следующие проблемы:
Проблема комбинирования макросов с выражениями делает написание макросов довольно трудным делом. Допустим, вы имеете следующий макрос с названием Листинг 3. Макрос, возвращающий минимум из двух значений
Возможно вы удивитесь, почему используется так много скобок. Из-за старшинства операторов. Например, если вы запишете К сожалению, еще есть и вторая проблема. Любая функция, вызванная как параметр, будет вызываться каждый раз, когда она появляется с правой стороны. Помните, препроцессор C не знает ничего о языке C и только выполняет текстовую подстановку. Следовательно, если вы выполните макрос Еще хуже, когда одно из этих вычислений имеет побочный эффект (например, распечатка, изменение глобальной переменной и т.д.), поскольку этот побочный эффект выполнится дважды. Эта проблема "множественных вызовов" может даже повлиять на возврат из макроса ошибочного значения, если одна из функций возвращает различное значение в каждом вызове. Более подробная информация по макропрограммированию препроцессора C доступна в "Справочном руководстве по CPP". Макропроцессор M4Макропроцессор M4 является одной из наиболее развитых систем текстовой макрообработки. Главным предметом его гордости является то, что он используется как вспомогательная инструментальная программа для конфигурационного файла популярной почтовой программы sendmail. Конфигурирование sendmail не является веселым или приятным занятием. Конфигурационному файлу программы sendmail посвящена целая книга. Однако создатели sendmail написали набор макросов M4 для облегчения процесса. В макросе вы просто указываете определенные параметры, а процессор M4 применяет шаблон, специфичный как для вашей локальной установки, так и для программы sendmail вообще. Таким образом, M4 создает конфигурационный файл за вас. Например, в листинге 4 приведена версия типичного конфигурационного файла sendmail, составленного на макросах M4: Листинг 4. Пример конфигурации sendmail с макросами M4
Вам не обязательно понимать это, но просто знайте, что этот маленький файл после обработки макропроцессором M4 генерирует 1,000 строк конфигурации. Аналогично, программа autoconf использует M4 для создания командных сценариев, основанных на простых макросах. Если вы когда-либо устанавливали программу, и первым вашим действием было выполнение сценария Листинг 5. Пример сценария autoconf, использующего макросы M4
При обработке макропроцессором создается командный сценарий, который будет выполнять стандартные проверки конфигурации, искать стандартные пути и команды компилятора, а также создавать для вас файлы Детали макропроцессора M4 слишком сложны для обсуждения в данной статье. Программы, которые пишут программыДавайте теперь переключим наше внимание с программ текстовой подстановки общего назначения на узкоспециализированные генераторы кода. Мы рассмотрим различные доступные программы, пример использования и создадим генератор кода. Обзор генераторов кодаСистемы GNU/Linux поставляются с несколькими программами для написания программ. Возможно наиболее популярны:
Эти программы генерируют тексты для языка C. Вы можете удивиться, почему они реализованы в виде генераторов кода, а не в виде функций. Тому есть несколько причин:
Каждое из этих инструментальных средств предназначено для создания конкретного типа программ. Bison используется для генерирования синтаксических анализаторов; Flex - для генерирования лексических анализаторов. Другие средства посвящены, в основном, автоматизации конкретных аспектов программирования. Например, интегрирование методов доступа к базе данных в императивные языки программирования часто является рутинной работой. Для ее облегчения и стандартизации предназначен Embedded SQL - система метапрограммирования, используемая для простого комбинирования доступа к базе данных и C. Хотя существует немало доступных библиотек, позволяющих обращаться к базам данных в C, использование такого генератора кода как Embedded SQL делает комбинирование C и доступа к базе данных намного более легким путем объединения SQL-сущностей в C в качестве расширения языка. Многие реализации Embedded SQL, однако, в основном являются простыми специализированными макропроцессорами, генерирующими обычные C-программы. Тем не менее, использование Embedded SQL делает для программиста доступ к базе данных более естественным, интуитивным и свободным от ошибок по сравнению с прямым использованием библиотек. При помощи Embedded SQL запутанность программирования баз данных маскируется макроязыком. Как использовать генератор кодаЧтобы увидеть генератор кода в работе, давайте рассмотрим короткую программу на Embedded SQL. Для этого нам необходим процессор Embedded SQL. База данных PostgreSQL поставляется с компилятором Embedded SQL - Листинг 6.Сценарий создания баз данных для примера программы
В листинге 7 приведена простая программа чтения и распечатки содержимого базы данных, отсортированного по полю Листинг 7. Пример программы c Embedded SQL
Если вы прежде работали с языком программирования C и обычной библиотекой базы данных, то можете сказать, что это намного более естественный способ кодирования. Нормальное C-кодирование не разрешает возврат нескольких значений произвольного типа, но наша строка Для компилирования и запуска программы просто поместите ее в файл с именем test.pgc и выполните при помощи следующих команд: Листинг 8. Создание программы с Embedded SQL
Создание генератора кодаТеперь, когда вы познакомились с несколькими типами генераторов кода и узнали, что они могут делать, можно приступить к написанию маленького генератора кода. Возможно самым простым генератором кода, который вы могли бы написать, является генератор, создающий статические таблицы преобразований. Часто, для того чтобы создать быстродействующие функции в C-программировании, вы просто создаете таблицу преобразования для всех возможных ответов. Это означает, что вам необходимо либо предварительно компилировать их вручную (что потребует затрат вашего времени), либо создавать их во время исполнения (что потребует затрат времени пользователя). В этом примере вы создадите генератор, который будет брать функцию или набор функций целого числа и создавать таблицы преобразования для ответа. Для представления о том, как сделать такую программу, давайте начнем с конца и будем работать в обратном направлении. Допустим, вам нужна таблица преобразования, возвращающая квадратные корни чисел между 5 и 20. Для генерирования подобной таблицы можно написать простую программу, например: Листинг 9. Генерирование и использование таблицы преобразования для квадратного корня
Теперь для преобразования ее в инициализированный статически массив вы должны удалить первую часть программы и заменить ее чем-то подобным следующему примеру (вычисления производятся вручную): Листинг 10. Программа вычисления квадратного корня со статической таблицей преобразования
Что нам нужно - программа, которая будет вычислять эти значения и выводить их в таблицу, аналогичную приведенной выше, для того чтобы можно было их загрузить во время компиляции. Давайте проанализируем различные части, с которыми мы здесь работали:
Все это очень простые, строго-определенные части - они могут быть просто записаны в виде обычного списка. Поэтому мы, возможно, захотим, чтобы наш вызов макроса комбинировал эти элементы в разделенный двоеточием список и выглядел примерно так: Листинг 11. Наш идеальный метод для генерирования компилируемой таблицы квадратных корней
Теперь нам просто нужна программа, преобразующая наш макрос в стандартную C-программу. Для этого простого примера используется Perl, поскольку он может вычислить код пользователя, записанный в строке, а его синтаксис во многом похож на синтаксис C. Это позволит загружать и обрабатывать код пользователя динамически. Наш генератор кода должен обрабатывать макрообъявления, но оставлять все отличные от макроса участки кода неизменными. Следовательно, основная организация макропроцессора должна выглядеть так:
В листинге 12 приведен Perl-код для создания нашего генератора таблицы: Листинг 12. Генератор кода для макроса
Для запуска этой программы выполните следующее: Листинг 13. Запуска генератора кода
Итак, всего в нескольких строках кода вы создали простой генератор кода, который значительно облегчает программирование. С этим простым макросом вы можете сильно сократить объем работы над любой программой, которая должна генерировать математическую таблицу, индексированную целым числом. Небольшая дополнительная работа могла бы также разрешить таблицы, содержащие полные объявления struct; поработав еще немного, можно гарантировать, что пространство не будет расходоваться на бесполезные пустые записи массива. Чувствительное к языку макропрограммирование с SchemeХотя генераторы кода немного понимают целевой язык, они обычно не являются полными синтаксическими анализаторами и не могут работать с другим целевым языком без перезаписи компилятора. Однако эта ситуация могла бы упроститься, если бы существовал язык, представленный простой структурой данных. В языке программирования Scheme сам язык представляется как связанный список, и он создан для обработки списков! Это делает Scheme идеальным (почти) языком создания программ, которые преобразуются - для синтаксического анализа программы не нужно ее объемного разбора. Scheme сам по себе является языком обработки списков. На самом деле возможности Scheme по выполнению преобразований выходят за эти рамки. Стандарт Scheme определяет макроязык, специально созданный для облегчения создания дополнений к языку. Большинство реализаций Scheme предоставляют дополнительные возможности для помощи в создании генерирующих код программ. Давайте еще раз рассмотрим проблемы наших C-макросов. Для макроса Листинг 14. Макрос обмена значений в Scheme
Это макрос В макросе
Затем указывается последовательность правил преобразования. Программа преобразования синтаксиса проходит по каждому правилу и пытается найти совпадающий шаблон. После его нахождения программа запускает указанное преобразование. В данном случае существует только один шаблон: С первого взгляда может показаться, что здесь имеются те же недостатки, что и в C-версии; однако есть несколько отличий. Во-первых, поскольку это язык Scheme, типы связаны с самими значениями, а не с именами переменных, поэтому абсолютно не надо беспокоиться о проблемах типов переменных, присутствующих в C-версии. Но нет ли здесь той же проблемы по именованию переменных, которая была ранее? То есть, если одна из переменных имеет имя На самом деле не должно быть никаких конфликтов. Макрос в Scheme, использующий Листинг 15. Возможное преобразование макроса перестановки значений
Как можно заметить, "гигиенический" макрос Scheme может предоставить вам преимущества других макросистем без многих их недостатков. Иногда, однако, вы не захотите, чтобы макрос был "гигиеническим". Например, вы можете захотеть ввести в макрос связывания, доступные преобразуемому коду. Простое объявление переменной не действует, поскольку система Макросы Давайте рассмотрим основную форму макроса Листинг 16. Макрос для генерирования значения или набора значений во время компиляции
Данная операция выполнится во время компиляции. А именно, она выполнится во время расширения макроса, что не всегда совпадает с временем компиляции в системах Scheme. Любое выражение, разрешенное во время компиляции на вашей системе Scheme, будет доступно для использования в этом выражении. Теперь посмотрим, как это работает. В
С этой возможностью выполнять вычисления во время компиляции можно создать версию макроса Листинг 17. Создание таблицы квадратных корней в Scheme
Его можно сделать даже еще более легким для использования, выполняя следующий макрос для построения таблицы, который будет очень похож на макрос языка C: Листинг 18. Макрос для создания таблиц преобразования во время компиляции
Теперь у вас есть функция, позволяющая легко построить любой тип таблиц. РезюмеУх! Мы рассмотрели большой объем материала, поэтому давайте потратим минутку на обзор. Сначала мы обсудили тип проблем, которые лучше всего решаются генерирующими код программами. К ним относятся:
Затем мы рассмотрели несколько систем метапрограммирования и примеры их использования. К ним относятся системы текстовой подстановки общего назначения, а также предметно-ориентированные генераторы программ и функций. Мы исследовали конкретный пример создания таблицы и написали генерирующую код программу построения статических таблиц в C. Наконец, мы познакомились с Scheme и увидели, как он может решать проблемы, которые возникают в языке C, используя конструкции, являющиеся частью самого языка Scheme. Scheme является одновременно и языком, и генератором собственного кода. Поскольку эти технологии встроены в сам язык, то упрощается программирование, и исчезают многие проблемы, присущие другим рассмотренным технологиям. Это позволяет легко и просто добавить предметно-ориентированные расширения к языку Scheme в областях, традиционно занятых генераторами кода. Во второй части данной серии статей мы более детально рассмотрим программирование макросов в Scheme и то, как они могут значительно облегчить ваши широкомасштабные задачи программирования. |