01 Изучаем макросы Nemerle - Квази-цитирование
От: VladD2 Российская Империя www.nemerle.org
Дата: 18.02.06 20:14
Оценка: 156 (19)
Итак, сейчас я расскажу, что такое квази-циритвание (quasi-quotation).

Квази-циритвание — это очень простая идея, но именно она делает макросы Nemerle-а мощнейшим и в тоже время относительно безопасным инструментом по сравнению с текстуальными макросами С/С++.

Прежде чем разбираться с тем, что такое квази-цитирование давайте сначала разберемся, что вообще означает термин «цитирование».

Цитирование (quotation)

Компилятор преобразует текст программы сначала в список токенов (умные ученые мужи еще называют их терминалами), а потом в так называемое абстрактное синтаксическое дерево (Abstract Syntax Tree, AST). AST – это дерево состоящее из объектов описывающих код.
В AST каждый элемент синтаксиса представлен веткой AST-дерева. В общем-то когда программист воспринимает код, то он тоже мысленно строит подобное дерево.
Я не специалист в AST Nemerle-a, так что продемонстрирую AST на базе AST R#-а. Вот как выглядит вот такой кусок кода:
if (a == 1)
  a = 2;

в AST R#-а:
RStatement statement = new RConditionStatement(new RBinaryOperatorExpression(
    new RUnresolvedReferenceExpression("a"), 
    RBinaryOperator.IdentityEquality, new RPrimitiveExpression("1")),
    new RExpressionStatement(new RAssignmentExpression(
    new RUnresolvedReferenceExpression("a"), RAssignmentOperator.Assign,
    new RPrimitiveExpression("1"))));

Переменная statement будет содержать AST-векту описывающую if.
Если теперь вызвать у statement метод ToString():
Console.WriteLine(statement.ToString())

то на консоль выведется текст:
if (a == 1)
  a = 2;

«Зачем такие сложности?» – спросят многие. А затем, что AST является объектной моделью с которой намного удобнее работать программно нежели с плоским текстом.
Преобразовав текст программы в AST компилятор выполняет над AST множество действий. Так он может преобразовать AST некоторой высокоуровневой конструкции вроде foreach в более низкоуровневую (например, в while). Так же компилятор вычисляет множество семантических атрибутов. Так он вычисляет типы переменных и выражений. Производит их сопоставление и контроль. В общем, творит с AST все что хочет. В итоге компилятор преобразует AST в промежуточное представление (например, в MSIL), а затем и в машинный код.

Зачем я рассказал обо все этом, спросите вы?
А затем, что цитирование (quotation) – это способ заставить компилятор преобразовать некоторый код в это самое AST. Мы конечно могли бы просто написать код создающий AST (если бы компилятор предоставлял бы нам доступ хотя бы к нему), но это, как вы могли заметить не очень приятное занятие.
С использованием цитирования мы можем резко упростить данный процесс.
Вот как будет выглядеть получение AST приведенного выше выражения (if) с использованием цитирования:
RStatement statement = <[
    if (a == 1)
        a = 2; ]>;

Не правда ли, значительно проще? Это синтаксис C#. Вернее гипотетического C#. C# в котором поддерживается цитирование. А вот как тоже самое выглядит на Nemerle:
def expr = <[
    when (a == 1)
        a = 2; ]>;

Как видите, на Nemerle это выглядит даже проще (что не может не радовать ).
Я использовал имя expr, так как в Nemerle if-это не предложение (statement) как в C#, а выражение (expression).
В отличии от C# мне не пришлось указывать тип переменной. Это можно благодаря тому, что Nemerle обладает мощнейшей функцией вывода типов.
Фактически тип переменной expr является PExpr. Это можно указать явно:
def expr : Nemerle.Compiler.Parsetree.PExpr = <[
  when (a == 1)
      a = 2; ]>;

Примечание: Естественно, что как и в C# указывать полный путь к типу не обязательно. Nemerle.Compiler.Parsetree можно было задать в using-е.

Теперь вы достаточно знаете чтобы понять суть макроса из первого сообщения этой темы. Напомню его код:
using System.Console;

macro TestMacro()
{
  WriteLine("compile-time");
  <[ WriteLine("run-time") ]>;
}

Что же мы видим в этом макросе?
А видим мы два вызова метода WriteLine(). Один просто внутри тела макроса, а второй внутри «кавычек».
Так как макрос вызывается во время компиляции, то первый вызов WriteLine():
WriteLine("compile-time");

выведет строку "compile-time" в окно вывода компилятора. Важно понят, что этот код, выполняемый во время компиляции. Я всего лишь вывел строчку в окно, но с тем же успехом я мог бы подсоединиться к БД и считать из нее метаинформацию. В общем, я волен делать в нем все что угодно.
Вторая строка кода не выполняется во время компиляции. Они заключена в «ковычки». Это приводит к тому, что компилятор преобразует ее в AST и его нам.
Так как Nemerle ориентирован на выражения, в нем не нужно писать «return», чтобы возвратить значение функции. Последнее выражение в последовательности автоматически становится возвращаемым значением функции (в данном случае макроса TestMacro()). Если бы в C# поддерживались макросы и квази-цитирование, то этот код записывался бы на C# примерно так:
macro RExpr TestMacro()
{
  WriteLine("compile-time");
  return <[ WriteLine("run-time") ]>;
}

думаю, вы уже без проблем должны проводить аналогии между C# и Nemerle-ом. Так что оставлю вам самим задачу угадать откуда взялся RExpr после ключевого слова macro .

Квази-цитирование (quasi-quotation)

Теперь настала пора понять, что же означает приставка «квази».
Изменим немного наш пример:
macro TestMacro(myName)
{
  WriteLine("Compile-time. myName = " + myName.ToString());
  <[ WriteLine("Run-time.\n Hallo, " + $myName) ]>;
}

За одно придется изменить и код применения макроса на следующий:
TestMacro("Vlad");
def a = "VladD2";
TestMacro(a);

Нажмем F5 находясь в файле содержащем последний код... Вот что мы получим при этом в окне вывода:
>build-and-run.cmd test-001
--# Compiling Macro-assembly #---
---------------------------------
--- Compiling target assembly ---
---------------------------------
Compile-time. myName = "Vlad"
Compile-time. myName = a
--------------- OK -----------------
--Run: test-001.exe
.
Run-time.
 Hallo, Vlad
Run-time.
 Hallo, VladD2
>Exit code: 0

Теперь разберемся с каждой строкой макроса по отедльности.
Первое что изменилось в макросе – это то, что мы добавили параметр «myName».
Опять же не думайте, что он не типизированный. Его тип Nemerle выведет из дальнейшего использования. Чуть позже мы вернемся к вопросу о типе параметра этого макроса а сейчас пока что разберем остальные строки.
В строке:
WriteLine("Compile-time. myName = " + myName.ToString());

Мы выводим в окно компилятора значение содержащееся в параметре myName преобразовывая его в строку.
Примечание: Все объекты Nemerle – это автоматически объекты .NET-а, так что не странно, что у них реализованы методы класса object.
Теперь давайте поглядим, что же выводят эти строки?
При вызове макроса следующим абазом:
TestMacro("Vlad");

указанная строчка выводит:
Compile-time. myName = "Vlad"

А при вызове его так:
def a = "VladD2";
TestMacro(a);

мы получаем:
Compile-time. myName = a

Почему же мы во втором случае не получили текстовое значение помещенное в переменную «a»? Дело в том, что myName – это не строковая переменная, а переменная содержащая AST-ветку описывающую передаваемое в макрос выражение. В первом случае нам был передан литерал, а во втором переменная. Именно по этому, мы видим то что видим.
Попробуем проверить мое предположение. Для этого добавим в макрос перед строчкой:
WriteLine("Compile-time. myName = " + myName.ToString());

еще одну стрчку:
WriteLine(myName.GetType());

она выведет реальный тип выражения. Выполним компиляцию и поглядим на результат:
---------------------------------
--- Compiling target assembly ---
---------------------------------
Nemerle.Compiler.Parsetree.PExpr+Literal
Compile-time. myName = "Vlad"
Nemerle.Compiler.Parsetree.PExpr+Ref
Compile-time. myName = a
...

Результат полностью подтверждает мои ожидания.
Остается разобраться, что же такое PExpr+Literal и PExpr+Ref.
Лезем в Рефлектор и видим, что это классы вложенные в класс PExpr являющиеся его наследниками. Таким образом мы можем сделать четкий вывод, что в макрос переданы AST-ветки описывающие выражения Nemerle. Причем в первом случае это строковый литерал, а во втором – ссылка на переменную.
Теперь перейдем к строке макроса:
  <[ WriteLine("Run-time.\n Hallo, " + $myName) ]>;

Эта строка и является конечной целью повествования этого рздела.
Она тоже изменилась. Как и в предыдущей строке в ней я попытался прибавить к константной строке строку, содержащуюся в параметре.
Но в данном случае присутствуют два важных отличия.
1. Я не вызываю метод ToString().
2. Перед именем параметра макроса стоит символ «$».
Возникает резонный вопрос, а почему собсвенно?
Дело в том, что для предыдущих строк myName являлся переменной. А для цитируемого кода myName переменной являться не будет. Если я напишу:
  <[ WriteLine("Run-time.\n Hallo, " + myName.ToString()) ]>;

то компиляция макроса пройдет «на ура», но при его применении я получу сообщения об ошибке:
...
test-001.n:4:1:4:10: error: unbound name `myName.ToString'
Nemerle.Compiler.Parsetree.PExpr+Ref
Compile-time. myName = a
test-001.n:6:1:6:10: error: unbound name `myName.ToString'
...

Причем, заметьте! Код макроса не смотря на ошибки выполняется. А сообщения об ошибках я получаю уже от компилятора компилирующего сгенерированный макросом код.
Так происходит потому, что области видимости имен макросов и области видимости кода из которых эти макросы вызываются не пересекаются. Макросы Nemerle-a являются так называемыми гигиеническими. Использовать имена из вызывающего кода можно, но для этого придется постараться. Это уберегает от ошибок связанных с перекрытием имен. Так что даже если мы в взывающем коде напишем:
def myName = "xxx";
TestMacro("Vlad");

то ничего не изменится и мы получим все то же сообщение:
test-001.n:5:1:5:10: error: unbound name `myName.ToString'

Что же делать? Как получить доступ к внешним именам?
Ответ – никак! Точенее этого не нужно делать.
Мы получили в качестве параметра выражение. И мы можем внедрить его внутрь возвращаемого макросом выражения.
В принципе, произвести такое внедрение можно написав код просматривающий формируемое AST и модифицирующий его. Но это довольно сложно. Чтобы упростить подобные операции и было придумано квази-цитирование!
Внутри цитируемого кода мы можем вставлять, скажем так, активный контент. То есть нечто, что будет вычисляться перед возвратом из макроса.
Именно этой возможностью я и воспользовался чтобы внедрить ссылку на параметр макроса внутрь генерируемого кода. Символ «$» перед именем «myName» в строке:
<[ WriteLine("Run-time.\n Hallo, " + $myName) ]>;

Как раз и указывает компилятору Nemerle-а, что имя myName нужно рассматривать как этот самый активный кнтент.
Получается, что это уже не просто цитирование. Вот по этому его и назвали квази-цитированием.

Наверно вам интересно, что же в итоге попадает в код исполняемого модуля? Мне тоже это интересно (хотя я и догадываюсь – что). По этому я декомпилировал полученный исполняемый файл. Вот что я увидил:
Console.WriteLine("Run-time.\n Hallo, Vlad");
string text1 = "VladD2";
Console.WriteLine("Run-time.\n Hallo, " + text1);

Обратите внимание, что в первом случае получилась константная строка, а во втором код конкатинации переменной со строкой. Несомненно первый случай является результатом оптимизаций компилятора Nemerle-a.
Не трудно догадаться, что если в качестве параметра макроса передать выражение, то компилятор подставит его вместо параметра.
А что будет если тип выражения окажется несовместимым со строкой?
Проведем смелый научный эксперимент и передадим макросу в качестве параметра выражение «1 + 2»:
TestMacro(1 + 2);

Компилятор отреагировал довольно ожидаемым образом:
test-001.n:5:1:5:10: error: each overload has an error during call:
test-001.n:5:1:5:10: error:   overload #1, method System.String.op_Addition(left : string, right : string) : string
test-001.n:5:1:5:10: error: in argument #2 (right), needed a string, got int: System.Int32 is not a subtype of System.String [simple require]
...

С нашими знаниями мы без труда определим, что макрос просто хочет строку в качестве параметра. Можно сказать, что вывод типов в Nemerle-е опять «рулит», но откровенно говоря за такую «рулежку» можно и в бубен схлопотать если нашим макросом воспользуется кто-то довольно здоровый и невыдержанный (как я) .
Что же делать?
Выхода есть два. И оба они хорошие. Самый простой четко указать в месте применения, что мы хотим иметь дело со строкой. Для этого обсуждаемую строку макроса нужно переписать следующим образом:
<[ WriteLine("Run-time.\n Hallo, " + ($myName : string)) ]>;

Если после этого скомпилировать код, то мы получим совершенно вменяемое сообщение об ошибке:
test-001.n:5:1:5:10: error: expected string, got int in type-enforced expression: System.Int32 is not a subtype of System.String [simple require]

В принципе, этого достаточно в большинстве случаев, но душа как всегда хочет большего. Можно ли попытаться добиться этого большего?
А как же? Мы же обладаем всей мощью компиляторостроителей! Просто изменим код следующим образом:
  <[ WriteLine("Run-time.\n Hallo, " + System.Convert.ToString($myName)) ]>;

и вуаля...
Теперь наш макрос стал значительно более гибким. Компилируем:
TestMacro(1 + 2);
TestMacro(1 + 2098098);
TestMacro("Vlad");
def a = "VladD2";
TestMacro(a);

и получаем:
>build-and-run.cmd test-001
--# Compiling Macro-assembly #---
---------------------------------
--- Compiling target assembly ---
---------------------------------
Nemerle.Compiler.Parsetree.PExpr+Call
Compile-time. myName = 1 + 2
Nemerle.Compiler.Parsetree.PExpr+Call
Compile-time. myName = 1 + 2098098
Nemerle.Compiler.Parsetree.PExpr+Literal
Compile-time. myName = "Vlad"
Nemerle.Compiler.Parsetree.PExpr+Ref
Compile-time. myName = a
--------------- OK -----------------
--Run: test-001.exe
.
Run-time.
 Hallo, 3
Run-time.
 Hallo, 2098099
Run-time.
 Hallo, Vlad
Run-time.
 Hallo, VladD2
>Exit code: 0

А что же у нес теперь «под копотом»? Сейчас декомпильнем :
Console.WriteLine("Run-time.\n Hallo, " + Convert.ToString((sbyte) 3));
Console.WriteLine("Run-time.\n Hallo, " + Convert.ToString(0x2003b3));
Console.WriteLine("Run-time.\n Hallo, " + Convert.ToString("Vlad"));
string text1 = "VladD2";
Console.WriteLine("Run-time.\n Hallo, " + Convert.ToString(text1));

А что если скормить нечто, что Convert-ору не по зубам:
TestMacro(System.Console);

Все в порядке! Мы получаем довольно разумное сообщение:
test-001.n:5:11:5:25: error: none of the meanings of `System.Console' meets the type ?:

И это мы еще не использовали таких возможностей макросов как обращение к информации о типах выражений и анализу AST!

В общем, даже на таком простом примере видно насколько мощной вещью являются макросы Nemerle.

Продолжение следует.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.