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

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

В предыдущей статье был приведен реальный пример из Nemerle. Лично мне сразу бросается в глаза один недостаток: вызов макроса неотличим от вызова обычной функции. Я считаю, что макрос - это совершенно новая сущность программирования, фундаментально отличающаяся от всего ранее известного, и программисту важно видеть, что в данной точке программы вызывается именно макрос, а не функция. Поэтому вызов макроса должен отличатся синтаксически.
В качестве удобного синтаксического маркера макросов (а также и всего, что относится к метапрограммированию) я решил использовать символ решетки #. Символ, понятный программистам С/С++ ("нечто, относящееся к препроцессору"), и еще незадействованный для других целей в других языках.

Итак, вызов нашего тестового макроса приобретает такой вид:
#TestMacro();

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

// testmacro.neo

macro TestMacro(myName)
{
    WriteLine("Compile-time: " + myName.ToString());
    <[ WriteLine("Run-time:
 " + #myName) ]>;
}

// test.neo
# TestMacro("Hello");

string a = "World";
#TestMacro(a);


Синтаксис уже немножко отличается от оригинального примера из замечательной статьи про Nemerle. Первое, на что следует обратить внимание - формат квазицитаты. Внутри квазицитаты появилось обращение к аргументу myName с использованием символа решетки # (в оригинале доллар $). Причина замены проста: раз уж решили, что решетка отвечает за метапрограммирование, то другие символы лучше не тратить (доллар вполне может использоваться для других целей, не связанных с метапрограммированием, и программист вполне может захотеть его "процитировать").

Теперь рассмотрим этот пример подробнее и по шагам.
Сначала этап компиляции. На этапе компиляции в консоль компилятора первый вызов макроса выведет строку
Compile-time: "Hello"
а второй вызов макроса выведет
Compile-time: a

Смысл этого очень простой: любые макросы всегда принимают в качестве аргументов кусочки дерева AST (то есть не "строки", "не "числа", не "типы" и т.д., а именно объекты AST !!!); в первом случае это дерево содержит единственный элемент, являющийся строкой "Hello", метод NExpr.ToString естественным путем возвращает это самое "Hello". Во втором случае дерево содержит единственный элемент - переменную "a",  метод NExpr.ToString возвращает ее имя - "a".

Теперь этап выполнения. Рассмотрим, что же макрос вставил в основной код с помощью квазицитаты.
С обычным содержимым цитаты все понятно - оно вставляется в код без изменений. Однако, иногда бывает нужно вставить в код что-либо внешнее, не являющееся частью цитаты. В нашем случае это аргумент макроса (который является частью макроса, а никак не генерируемого кода). Если бы не символ решетки перед myName, макрос сгенерировал бы код

WriteLine("Run-time: " + myName);

который, естественно, не компилировался бы, потому что никакой переменной myName в основной программе (test.neo) нет (а если бы такая переменная была - мы бы имели очень опасную возможность трудноконтролируемого доступа к внешним переменным, опасность перекрытия имен и т.д. - поэтому и в Nemerle, и в Neo, и во всех других реализациях макросов все локальные имена макроса намеренно выведены в специальную область видимости, недоступную из основного кода; это называется гигеничность макросов)

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

Эта возможность очень близка к сплайс-строкам в языках типа PHP:



$who = "World";
$str = "Hello $who";
echo $str;

Строка $str является сплайс-строкой и вычисляется, вместо всех $-имен в нее подставляются соответствующие значения переменных из текущей области видимости.

В нашем случае в первый вызов макроса передается AST-объект, содержащий константную строку "Hello". Этот кусочек дерева AST встраивается вместо #myName в цитируемое дерево, и получается код вида


WriteLine("Run-time: " + "Hello");


который и вставляется в целевое AST файла test.neo. В результате выводится закономерное "Run-time: Hello".



Во второй вызов макроса передается AST-объект, содержащий переменную "a". Аналогично, в дерево квазицитаты встраивается фрагмент, содержащий переменную "a", и в итоге в AST файла test.neo вставляется модифицированная квазицитата, соответствующая коду



WriteLine("Run-time: " + a);



Именно этот код и компилируется в итоге в двоичное предствление целевой программы.


Понимание того, что аргументы макросов (а также шаблонов) - именно фрагменты AST, очень здорово облегчает понимание того, как макросы работают. 

No comments:

Post a Comment