Метапрограммирование - макросы 1 - введение

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

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

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

Что такое плагины - думаю известно всем. Это некие модули, программы, рассчитанные не на самостоятельное исполнение, а на работу внутри какой-то другой программы (в этом смысле все обычные программы - плагины для ОС :) ). Бывают плагины к аудио и видеоплеерам, графическим и звуковым редакторам, текстовым редакторам и средам разработки. А что-же такое плагины к компилятору? По сути, то-же самое: некий модуль, рассчитанный на работу внутри компилятора. Основная идея макросов как плагинов к компилятору заключается в том, что макросы - это также часть разрабатываемой программы, они существуют в исходниках рядом с исходниками основной программы (или даже в одном файле с обычными исходниками!). То есть компилятор неким образом анализирует исходник, компилирует сначала макросы, в случае успешной компиляции подключает их к себе и уже с их помощью компилирует остальные исходники программы.

Прежде чем начинать тему, следует рассмотреть смежные технологии, которые очень тесно переплетаются с макросами.
1. Условная компиляция. Знаменитые #if, #else, #endif и т.д. Эти операторы по сути - тоже макросы, точнее - макрооператоры, встроенные в компиоятор и не требущие компиляции. В С/С++ это часть препроцессора - специальной программы, рассматривающей исходник как простой текстовый файл и генерирующей другой текстовый файл - обработанный исходник. Такой подход я считаю кривым и непреемлемым в современном программировании. Тем ни менее, операторы условной компиляции иногда бывают полезными.
2. Шаблоны. Шаблон - параметризированная сущность (тип данных, функция, метод). В качестве параметра используется любая константа времени компиляции (то есть или собственно числовая или строковая константа, или тип данных, потенциально возможно использование функций, блоков кода, имен переменных). Шаблоны раскрываются на этапе компиляции. Упрощенно, компилятор генерирует код, подставляя фактические параметры шаблона вместо шаблонных переменных.
3. Рефлексия и атрибуты. Рефлексия - это получение информации о программе средствами самйо программы. Вся информация, предоставляемая рефлексией, относится к информации времени компиляции (простейший пример - имена переменных), поэтому данные рефлексии могут быть использованы в метапрограммировании. Атрибуты - это пользовательские данные рефлексии, прикпепляемые к объектам на этапе компиляции (поэтому атрибуты в каком-то смысле подобны макросам).

Теперь рассмотрим собственно макросы. Данный пример взят из языка Nemerle. Это самый простой пример, который я смог найти, и он отлично демонстрирует суть макросов. Пусть будет некий файл TestMacro.n, содержащий такой код:
macro TestMacro()
{
    WriteLine("compile-time\n");
    <[ WriteLine("run-time\n") ]>;
}


Допустим, есть другой файл test.n, в котором есть такая строчка
TestMacro();

Если оба файла включены в проект, то при попытке сборки  в лог сообщений (Output) компилятора  будет выведено сообщение "compile-time".  А при запуске получившейся программы в консоль будет выведено сообщение "run-time".

Макросы, как мы помним, выполняются в процессе работы компилятора. Компилятор сначала компилирует все макросы и запоминает их имена в своих внутренних таблицах. Затем он начинает компилировать обычный код. Если в обычном коде попадается вызов макроса, то комплиятор обращается к своей внутренней таблице макросов, находит там макрос и выполняет его как часть своего кода. Поэтому сообщение "compile time" выводится на консоль компилятора, наряду с другими сообщениями компилятора.

Далее нам следует обратить внимание на специальные скобки <[ ]>, в которые заключена вторая строчка макроса. Такие скобки называются квази-цитированием. С помощью таких скобок можно брать фрагменты кода как-бы в кавычки, по аналогии с обычными строками. Только в отличие от строк, фрагменты кода - это древовидные структуры AST (Abstract Syntax Tree), и они вставляются не в исходный текст программы, а в одну из ее промежуточных форм - абстрактное синтаксическое дерево. В нашем случае код " WriteLine("run-time\n")"  вставляется в точку вызова макроса. Это очень похоже на препроцессор С/С++, но с одним очень важным отличием: и вставляемый код, и код, в который осуществляется вставка, существует уже не в виде текста, а в частично откомпилированном виде - в форме деревьев AST в памяти компилятора. Это очень важное отличие, дающее огромное количество различных преимуществ.

Приставка "квази" указывает на потенциальную возможность изменять фрагменты кода из самого макроса. Например, вместо строки "run-time" макрос мог бы гененировать строку с датой и временем компиляции программы и вставлять ее в код. В результате получалась бы статическая строка (что важно!), но при каждой компиляции она была бы новая (по сути - аналог __DATE__ и __TIME__ из C/C++). В данном случае это просто цитирование, так как фрагмент вставляется без изменений.

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





No comments:

Post a Comment