Искусство метапрограммирования, Часть 1: Введение в метапрограммирование

Джонатан Бартлет

Оглавление 

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

Генерирующие код программы часто называют метапрограммами ; написание этих программ называется метапрограммированием. Создание программ, генерирующих код, имеет многочисленные применения.

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

Различные применения метапрограммирования

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

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

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

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

Основные текстовые макроязыки

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

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

Препроцессор C (CPP)

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

Если вы работали с языком C, то, возможно, имели дело с макросом #define. Текстовые макрорасширения хотя и не идеальный, но простой способ выполнить метапрограммирование на начальном уровне во многих языках, которые не имеют более развитых возможностей генерирования кода. В листинге 1 приведен пример макроса #define:

Листинг 1. Простой макрос для перестановки двух значений

#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }

Этот макрос дает вам возможность переставить два значения данного типа. Эту операцию лучше всего делать в макросе по нескольким причинам:

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

В листинге 2 приведен пример используемого макроса:

Листинг 2. Использование макроса SWAP:

#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }
int main()
{
    int a = 3;
    int b = 5;
    printf("a is %d and b is %d\n", a, b);
    SWAP(a, b, int);
    printf("a is now %d and b is now %d\n", a, b);

    return 0;
}

Препроцессор C во время исполнения дословно изменяет текст SWAP(a, b, int) на { int __tmp_c; __tmp_c = b; b = a; a = __tmp_c; }.

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

  • Текстовая подстановка может стать очень запутанной при использовании в комбинации с другими выражениями.
  • Препроцессор C разрешает использовать только фиксированное количество аргументов макросов.
  • Из-за механизма работы с типами языка C вам часто требуются различные макросы для различных типов аргументов, или, по крайней мере, вы должны передавать типы параметров как аргументы макроса.
  • Поскольку выполняется только текстовая подстановка, язык C недостаточно интеллектуален для переименования нашей временной переменной, если она конфликтует с одним из переданных в макрос аргументов. Наш макрос наверняка выдаст ошибку, если ему передать переменную с названием __tmp_c.

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

Листинг 3. Макрос, возвращающий минимум из двух значений

#define MIN(x, y) ((x) > (y) ? (y) : (x))

Возможно вы удивитесь, почему используется так много скобок. Из-за старшинства операторов. Например, если вы запишете MIN(27, b=32), без этих скобок макрос расширился бы в 27 > b = 32 ? b = 32 : 27, что привело бы к ошибке компиляции, поскольку выражение 27 > b выполнилось бы раньше из-за старшинства операций. Если опять поставить скобки, то все будет работать так, как ожидалось.

К сожалению, еще есть и вторая проблема. Любая функция, вызванная как параметр, будет вызываться каждый раз, когда она появляется с правой стороны. Помните, препроцессор C не знает ничего о языке C и только выполняет текстовую подстановку. Следовательно, если вы выполните макрос MIN(do_long_calc(), do_long_calc2()), то он расширится в ( (do_long_calc()) > (do_long_calc2()) ? (do_long_calc2()) : (do_long_calc())). Его выполнение займет длительное время, поскольку как минимум одно вычисление будет выполнено дважды.

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

Более подробная информация по макропрограммированию препроцессора C доступна в "Справочном руководстве по CPP".

Макропроцессор M4

Макропроцессор M4 является одной из наиболее развитых систем текстовой макрообработки. Главным предметом его гордости является то, что он используется как вспомогательная инструментальная программа для конфигурационного файла популярной почтовой программы sendmail.

Конфигурирование sendmail не является веселым или приятным занятием. Конфигурационному файлу программы sendmail посвящена целая книга. Однако создатели sendmail написали набор макросов M4 для облегчения процесса. В макросе вы просто указываете определенные параметры, а процессор M4 применяет шаблон, специфичный как для вашей локальной установки, так и для программы sendmail вообще. Таким образом, M4 создает конфигурационный файл за вас.

Например, в листинге 4 приведена версия типичного конфигурационного файла sendmail, составленного на макросах M4:

Листинг 4. Пример конфигурации sendmail с макросами M4

divert(-1)
include(`/usr/share/sendmail-cf/m4/cf.m4')
VERSIONID(`linux setup for my Linux dist')dnl
OSTYPE(`linux')
define(`confDEF_USER_ID',``8:12'')dnl
undefine(`UUCP_RELAY')dnl
undefine(`BITNET_RELAY')dnl
define(`PROCMAIL_MAILER_PATH',`/usr/bin/procmail')dnl
define(`ALIAS_FILE', `/etc/aliases')dnl
define(`UUCP_MAILER_MAX', `2000000')dnl
define(`confUSERDB_SPEC', `/etc/mail/userdb.db')dnl
define(`confPRIVACY_FLAGS', `authwarnings,novrfy,noexpn,restrictqrun')dnl
define(`confAUTH_OPTIONS', `A')dnl
define(`confTO_IDENT', `0')dnl
FEATURE(`no_default_msa',`dnl')dnl
FEATURE(`smrsh',`/usr/sbin/smrsh')dnl
FEATURE(`mailertable',`hash -o /etc/mail/mailertable.db')dnl
FEATURE(`virtusertable',`hash -o /etc/mail/virtusertable.db')dnl
FEATURE(redirect)dnl
FEATURE(always_add_domain)dnl
FEATURE(use_cw_file)dnl
FEATURE(use_ct_file)dnl
FEATURE(local_procmail,`',`procmail -t -Y -a $h -d $u')dnl
FEATURE(`access_db',`hash -T<TMPF> -o /etc/mail/access.db')dnl
FEATURE(`blacklist_recipients')dnl
EXPOSED_USER(`root')dnl
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')
FEATURE(`accept_unresolvable_domains')dnl
MAILER(smtp)dnl
MAILER(procmail)dnl
Cwlocalhost.localdomain

Вам не обязательно понимать это, но просто знайте, что этот маленький файл после обработки макропроцессором M4 генерирует 1,000 строк конфигурации.

Аналогично, программа autoconf использует M4 для создания командных сценариев, основанных на простых макросах. Если вы когда-либо устанавливали программу, и первым вашим действием было выполнение сценария ./configure, возможно, вы использовали программу, сгенерированную при помощи макроса autoconf. Листинг 5 - это простая autoconf-программа, генерирующая программу configure длиной свыше 3,000 строк:

Листинг 5. Пример сценария autoconf, использующего макросы M4

AC_INIT(hello.c)
AM_CONFIG_HEADER(config.h)
AM_INIT_AUTOMAKE(hello,0.1)
AC_PROG_CC
AC_PROG_INSTALL
AC_OUTPUT(Makefile)

При обработке макропроцессором создается командный сценарий, который будет выполнять стандартные проверки конфигурации, искать стандартные пути и команды компилятора, а также создавать для вас файлы config.h и Makefile из шаблонов.

Детали макропроцессора M4 слишком сложны для обсуждения в данной статье.

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

Давайте теперь переключим наше внимание с программ текстовой подстановки общего назначения на узкоспециализированные генераторы кода. Мы рассмотрим различные доступные программы, пример использования и создадим генератор кода.

Обзор генераторов кода

Системы GNU/Linux поставляются с несколькими программами для написания программ. Возможно наиболее популярны:

  • Flex, генератор лексического анализатора
  • Bison, генератор синтаксического анализатора
  • Gperf, развитый генератор хэш-функции

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

  • Входные параметры для этих функций являются очень сложными и их непросто выразить в виде, корректном для 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 - ecpg. Для запуска этой программы вы должны создать базу данных в PostgreSQL под названием "test". Затем в этой базе данных выполните следующие команды:

Листинг 6.Сценарий создания баз данных для примера программы

create table people (id serial primary key, name varchar(50));
insert into people (name) values ('Tony');
insert into people (name) values ('Bob');
insert into people (name) values ('Mary');

В листинге 7 приведена простая программа чтения и распечатки содержимого базы данных, отсортированного по полю name:

Листинг 7. Пример программы c Embedded SQL

#include <stdio.h>
int main()
{
   /* Установить соединение с базой данных - замените postgres/password на
      username/password для вашей системы*/
   EXEC SQL CONNECT TO unix:postgresql://localhost/test USER postgres/password;

   /* Эти переменные будут использоваться для временного хранения с базой данных */
   EXEC SQL BEGIN DECLARE SECTION;
   int my_id;
   VARCHAR my_name[200];
   EXEC SQL END DECLARE SECTION;

   /* Это команда, которую мы собираемся выполнить */
   EXEC SQL DECLARE test_cursor CURSOR FOR
      SELECT id, name FROM people ORDER BY name;

   /* Выполнение команды */
   EXEC SQL OPEN test_cursor;

   EXEC SQL WHENEVER NOT FOUND GOTO close_test_cursor;
   while(1) /* наша предыдущая команда будет обрабатывать выход из цикла */
   {
      /* Извлечь следующее значение */
      EXEC SQL FETCH test_cursor INTO :my_id, :my_name;
      printf("Fetched ID is %d and fetched name is %s\n", my_id, my_name.arr);
   }

   /* Очистка */
   close_test_cursor:
   EXEC SQL CLOSE test_cursor;
   EXEC SQL DISCONNECT;

   return 0;
}

Если вы прежде работали с языком программирования C и обычной библиотекой базы данных, то можете сказать, что это намного более естественный способ кодирования. Нормальное C-кодирование не разрешает возврат нескольких значений произвольного типа, но наша строка EXEC SQL FETCH делает именно это.

Для компилирования и запуска программы просто поместите ее в файл с именем test.pgc и выполните при помощи следующих команд:

Листинг 8. Создание программы с Embedded SQL

ecpg test.pgc
gcc test.c -lecpg -o test
./test

Создание генератора кода

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

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

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

Листинг 9. Генерирование и использование таблицы преобразования для квадратного корня

/* наша таблица преобразований */
double square_roots[21];

/* функция для загрузки таблицы во время исполнения */
void init_square_roots()
{
   int i;
   for(i = 5; i < 21; i++)
   {
      square_roots[i] = sqrt((double)i);
   }
}

/* программа, которая использует таблицу */
int main ()
{
   init_square_roots();
   printf("The square root of 5 is %f\n", square_roots[5]);
   return 0;
}

Теперь для преобразования ее в инициализированный статически массив вы должны удалить первую часть программы и заменить ее чем-то подобным следующему примеру (вычисления производятся вручную):

Листинг 10. Программа вычисления квадратного корня со статической таблицей преобразования

double square_roots[] = {
   /* здесь мы пропускаем  */ 0.0, 0.0, 0.0, 0.0, 0.0
   2.236068, /* квадратный корень 5 */
   2.449490, /* квадратный кореньt 6 */
   2.645751, /* квадратный корень 7 */
   2.828427, /* квадратный корень 8 */
   3.0, /* квадратный корень 9 */
   ...
   4.472136 /* квадратный корень 20 */
};

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

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

  • Имя массива
  • Тип массива
  • Начальный индекс
  • Конечный индекс
  • Значение по умолчанию для пропущенных записей
  • Выражение для вычисления конечного значения

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

Листинг 11. Наш идеальный метод для генерирования компилируемой таблицы квадратных корней

/* sqrt.in */
/* Наш вызов макроса для построения таблицы.  Формат такой: */
/* TABLE:имя массива:тип:начальный индекс:конечный индекс:по умолчанию:выражение */
/* VAL используется как метка для текущего индекса в выражении */
TABLE:square_roots:double:5:20:0.0:sqrt(VAL)

int main()
{
   printf("The square root of 5 is %f\n", square_roots[5]);
   return 0;
}

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

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

  1. Прочитать строку.
  2. Нужно обрабатывать строку?
  3. Если да, - обработать строку и сгенерировать результат.
  4. Если нет, - скопировать строку прямо в результат, ничего не меняя.

В листинге 12 приведен Perl-код для создания нашего генератора таблицы:

Листинг 12. Генератор кода для макроса

#!/usr/bin/perl
#
#tablegen.pl
#

##Принимает каждую строку программы в $line
while(my $line = <>)
{
   #Это вызов макроса?
   if($line =~ m/TABLE:/)
   {
      #Если да, разделить на отдельные компоненты
      my ($dummy, $table_name, $type, $start_idx, $end_idx, $default,
         $procedure) = split(m/:/, $line, 7);

      #Главным отличием между C и Perl в математических выражениях является то, что для
      #Perl в начало переменной добавляется знак доллара, т.е. мы будем добавлять его здесь
      $procedure =~ s/VAL/\$VAL/g;

      #Вывести объявление массива
      print "${type} ${table_name} [] = {\n";

      #Идти по каждому элементу массива
      foreach my $VAL (0 .. $end_idx)
      {
         #Обрабатывать ответ только при достижении начального индекса
         if($VAL >= $start_idx)
         {
            #вычислить указанную процедуру (устанавливает $@ при возникновении любых ошибок)
            $result = eval $procedure;
            die("Error processing: $@") if $@;
         }
         else
         {
            #если мы не достигли начального индекса, использовать значение по умолчанию
            $result = $default;
         }

         #Вывести значение
         print "\t${result}";

         #Если есть еще строки для обработки, добавляем запятую после значения
         if($VAL != $end_idx)
         {
            print ",";
         }

         print "\n"
      }

      #Завершить объявление
      print "};\n";
   }
   else
   {
      #Если это не вызов макроса, просто копируем строку
      print $line;
   }
}

Для запуска этой программы выполните следующее:

Листинг 13. Запуска генератора кода

./tablegen.pl < sqrt.in > sqrt.c
gcc sqrt.c -o sqrt
./a.out

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

Чувствительное к языку макропрограммирование с Scheme

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

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

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

Давайте еще раз рассмотрим проблемы наших C-макросов. Для макроса SWAP вы, во-первых, должны явно указать типы меняемых значений, и, во-вторых, вы должны использовать такое имя для временной переменной, которое, вы уверены, не используется где-либо еще. Рассмотрим, как выглядит эквивалент на языке Scheme, и как Scheme решает эти проблемы:

Листинг 14. Макрос обмена значений в Scheme

;;Определить SWAP как макрос
(define-syntax SWAP
   ;;Мы используем метод syntax-rules для создания макроса
   (syntax-rules ()
      ;;Rule Group
      (
         ;;Это шаблон, соответствие которому мы проверяем
         (SWAP a b)
         ;;Во что мы его преобразовываем
         (let (
               (c b))
            (set! b a)
            (set! a c)))))

(define first 2)
(define second 9)
(SWAP first second)
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)

Это макрос syntax-rules. В Scheme существует несколько макросистем, но syntax-rules является стандартом.

В макросе syntax-rules define-syntax является ключевым словом, используемым для определения преобразования. После ключевого слова define-syntax указывается имя определяемого макроса, а затем указывается преобразование.

syntax-rules - это тип применяемого преобразования. Внутри скобок находятся любые другие специфичные для макроса символы, отличные от имени самого макроса (в данном случае их нет).

Затем указывается последовательность правил преобразования. Программа преобразования синтаксиса проходит по каждому правилу и пытается найти совпадающий шаблон. После его нахождения программа запускает указанное преобразование. В данном случае существует только один шаблон: (SWAP a b). a и b - это переменные шаблона , которые соответствуют элементам кода в операторе вызова макроса и используются для перестановки во время преобразования.

С первого взгляда может показаться, что здесь имеются те же недостатки, что и в C-версии; однако есть несколько отличий. Во-первых, поскольку это язык Scheme, типы связаны с самими значениями, а не с именами переменных, поэтому абсолютно не надо беспокоиться о проблемах типов переменных, присутствующих в C-версии. Но нет ли здесь той же проблемы по именованию переменных, которая была ранее? То есть, если одна из переменных имеет имя c, не вызовет ли это конфликта?

На самом деле не должно быть никаких конфликтов. Макрос в Scheme, использующий syntax-rules, является " гигиеническим ". Это означает, что все временные переменные, используемые в макросе, автоматически переименовываются перед подстановкой , для того чтобы предотвратить конфликт имен. Следовательно, в этом макросе с переименуется во что-нибудь еще перед подстановками, если имя одной из переменных для подстановки равно c. На самом деле, вероятнее всего, он будет переименован в любом случае. В листинге 15 представлен возможный результат макропреобразования программы:

Листинг 15. Возможное преобразование макроса перестановки значений

(define first 2)
(define second 9)
(let
   (
      (__generated_symbol_1 second))
   (set! second first)
   (set! first __generated_symbol_1))
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)

Как можно заметить, "гигиенический" макрос Scheme может предоставить вам преимущества других макросистем без многих их недостатков.

Иногда, однако, вы не захотите, чтобы макрос был "гигиеническим". Например, вы можете захотеть ввести в макрос связывания, доступные преобразуемому коду. Простое объявление переменной не действует, поскольку система syntax-rules просто переименует переменную. Поэтому большинство схем имеют также "негигиеническую" макросистему с названием syntax-case.

Макросы syntax-case писать труднее, но они намного мощнее, поскольку для ваших преобразований в определенной степени доступна вся исполняющая система Scheme. Макросы syntax-case не являются стандартом, но они реализованы на многих системах Scheme. Те же, которые не имеют syntax-case, обычно имеют другие аналогичные системы.

Давайте рассмотрим основную форму макроса syntax-case. Определим макрос с именем at-compile-time, который будет выполнять данную форму во время компиляции.

Листинг 16. Макрос для генерирования значения или набора значений во время компиляции

;;Определить макрос
(define-syntax at-compile-time
   ;;x - это синтаксический объект для преобразования
   (lambda (x)
      (syntax-case x ()
         (
            ;;Шаблон, аналогичный шаблону syntax-rules
            (at-compile-time expression)

            ;;with-syntax позволяет нам создавать синтаксические объекты
            ;;динамически
            (with-syntax
               (
                  ;это - создаваемый нами синтаксический объект
                  (expression-value
                     ;после вычисления выражения преобразуем его в синтаксический объект
                     (datum->syntax-object
                        ;домен syntax
                        (syntax at-compile-time)
                        ;отметить значение кавычками, поскольку оно является литеральным значением
                        (list 'quote
                        ;вычислить значение преобразования
                           (eval
                              ;;преобразовать выражение из синтаксического представления
                              ;;в список
                              (syntax-object->datum (syntax expression))
                              ;;среда для вычисления
                              (interaction-environment)
                              )))))
               ;;Просто возвратить сгенерированное значение как результат
               (syntax expression-value))))))

(define a
   ;;преобразовать в 5 во время компиляции
   (at-compile-time (+ 2 3)))

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

В syntax-case вы, фактически, определяете функцию преобразования - lambda. x - это преобразуемое выражение. with-syntax определяет дополнительные синтаксические элементы, которые могут быть использованы в выражении преобразования. syntax берет синтаксические элементы и комбинирует их вместе, следуя тем же правилам, что и программа преобразования в syntax-rules. Давайте пошагово рассмотрим, что происходит:

  1. Выражение at-compile-time совпадает.
  2. В самой внутренней части преобразования expression преобразуется в список представлений и вычисляется как обычный код схемы.
  3. Результат объединяется с символом "кавычки" в список, для того чтобы Scheme обрабатывал его как литеральное значение, когда он станет кодом.
  4. Эти данные преобразуются в синтаксический объект.
  5. Синтаксическому объекту дается имя expression-value для выражения его в результате работы.
  6. Программа преобразования (syntax expression-value) указывает, что expression-value является нераздельным результатом из этого макроса.

С этой возможностью выполнять вычисления во время компиляции можно создать версию макроса TABLE, даже еще лучшую, чем при использовании языка C. В листинге 17 показано, как вы могли бы сделать это в Scheme с нашим макросом at-compile-time:

Листинг 17. Создание таблицы квадратных корней в Scheme

(define sqrt-table
   (at-compile-time
      (list->vector
         (let build
            (
               (val 0))
            (if (> val 20)
               '()
               (cons (sqrt val) (build (+ val 1))))))))

(display (vector-ref sqrt-table 5))
(newline)

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

Листинг 18. Макрос для создания таблиц преобразования во время компиляции

(define-syntax build-compiled-table
   (syntax-rules ()
      (
         (build-compiled-table name start end default func)
         (define name
            (at-compile-time
               (list->vector
                  (let build
                     (
                        (val 0))
                     (if (> val end)
                        '()
                        (if (< val start)
                           (cons default (build (+ val 1)))
                           (cons (func val) (build (+ val 1))))))))))))

(build-compiled-table sqrt-table 5 20 0.0 sqrt)
(display (vector-ref sqrt-table 5))
(newline)

Теперь у вас есть функция, позволяющая легко построить любой тип таблиц.

Резюме

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

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

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

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

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=2559