Мистецтво метапрограмування

Зміст



Однією з найбільш невикористовуваних технологій програмування є написання програм, що генерують програми або частини програм. Дізнатися, чому метапрограмування необхідно, і розгляньте деякі його компоненти (текстові макромови, спеціалізовані генератори коду). Дізнайтеся, як створити генератор коду, і познайомтеся ближче з чутливим до мови макропрограмування в 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
“, a, b);
SWAP(a, b, int);
printf(“a is now %d and b is now %d
“, a, b);

return 0;
}


Препроцесор C під час виконання дослівно змінює текст SWAP(a, b, int) на { int __tmp_c; __tmp_c = b; b = a; a = __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 поставляються з декількома програмами для написання програм. Можливо найбільш популярні:



Ці програми генерують тексти для мови 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
“, 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
“, 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
“, 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} [] = {
“;

# Йти по кожному елементу масиву
foreach my $VAL (0 .. $end_idx)
{
# Обробляти відповідь тільки при досягненні початкового індексу
if($VAL >= $start_idx)
{
# Обчислити зазначену процедуру (встановлює $ @ при виникненні будь-яких помилок)
$result = eval $procedure;
die(“Error processing: $@”) if $@;
}
else
{
# Якщо ми не досягли початкового індексу, використовувати значення за замовчуванням
$result = $default;
}

# Вивести значення
print ” ${result}”;

# Якщо є ще рядка для обробки, додаємо кому після значення
if($VAL != $end_idx)
{
print “,”;
}

print ”

}

# Завершити оголошення
print “};
“;
}
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 і те, як вони можуть значно полегшити ваші широкомасштабні завдання програмування.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*