Единственный способ развеить скепсис — это попробовать.
Я решил попробовать макросы Nemerle в более-менее реальной задаче. Цель две.
1. Изучить собственно Nemerle и его макросы.
2. Продемонстрировать его возможности на этом форуме.
Я планирую несколько сообщений которые будут являться подсообщениями этого.
Так как многие из вас захотят попробовать приводимый мною код, то начнем пожалуй с подготовительного этапа. Конечно в реальных условиях многие из вас предпочту полноценную IDE и какую-нибудь утилиту сборки (make, Ant или MSBuild), но учитывая, что для данного случая скорее важна простота вхождения, я решил остановиться на Сцинтиле как редакторе и обычном бач-файле для сборки проекта. Ведь нам пока что достаточно компилировать два файла (сборку с макросами и целевую сборку).
Итак, для экспериментов вам понадобится:
1. .NET Framework 2.0 (и возможно SDK). Лучше конечно если на машине просто установлена VS 2005.
2. Компилятор Nemerle и сопутствующая ему байда. Все это можно взять на их сайте
http://nemerle.org. Так здесь
http://nemerle.org/download/msi/nemerle-0.9.2.msi лежит инсталлятор для Виндовс. А здесь
http://nemerle.org/download/ можно взять более свежую рабочую версию причем вместе с исходниками. Очень советую это сделать даже если вы выберете инсталляцию msi-я, так как поглядеть на исходники макросво и вообще на исподники созданные авторами языка очень полезно. Полноценной документации ведь пока нет, а исходники говорят о многом.
2. Сцинтила или другой редактор который упростить ввод и тестирование кода. Сцентилу можно взять здесь:
http://prdownloads.sourceforge.net/scintilla/wscite167.zip?use_mirror=heanet
Файлы для обеспечения подсветки синтаксиса Nemerle для Сцинтылы можно взять здесь:
http://nemerle.org/svn/nemerle/trunk/misc/scintilla/SciTEGlobal.properties
http://nemerle.org/svn/nemerle/trunk/misc/scintilla/nemerle.properties
их нужно скопировать в каталог где лежат другие properties-файлы Сцинтилы.
3. Для упрощения компиляции и запуска файлов я написал простенький cmd-файлик для ХРюши:
@echo off
SET FileName=%1
SET MacroOutFileName=%FileName%-macro.dll
SET MacroSourceFileName=%FileName%-macro.n
SET SourceFileName=%FileName%.n
SET OutFileName=%FileName%.exe
if exist %MacroSourceFileName% (
@echo --# Compiling Macro-assembly #---
@echo ---------------------------------
ncc.exe -no-color -debug+ -r Nemerle.Compiler.dll -t:dll %MacroSourceFileName% -out %MacroOutFileName%
if not errorlevel 1 (
@echo --- Compiling target assembly ---
@echo ---------------------------------
ncc.exe -no-color -debug+ -r %MacroOutFileName% -t exe %SourceFileName% -o %OutFileName%
rem -r System.Windows.Forms -r System.Data
if errorlevel 1 (
@echo -------------------------------------
@echo !!! Error detected in targer code !!!
) else (
@echo --------------- OK -----------------
@echo --Run: %OutFileName%
@echo .
%OutFileName%
)
) else (
@echo ------------------------------------
@echo !!! Error detected in macro-code !!!
)
) else (
@echo ----- No Macro-assembly found ------
@echo ------------------------------------
if not errorlevel 1 (
@echo --- Compiling target assembly ---
@echo ---------------------------------
ncc.exe -no-color -debug+ -t exe %SourceFileName% -o %OutFileName%
rem -r System.Windows.Forms -r System.Data
if errorlevel 1 (
@echo -------------------------------------
@echo !!! Error detected in targer code !!!
) else (
@echo --------------- OK -----------------
@echo --- Run: %OutFileName%
@echo .
%OutFileName%
)
) else (
@echo ------------------------------------
@echo --! Error detected in macro-code !--
)
)
Его нужно сохранить под именеме build-and-run.cmd в каталог где лежат бинарники Нэмерла (файл ncc.exe и т.п.).
Далее нужно отредактировать файл nemerle.properties добавив в него примерно такие строчки:
#Extra tool: Build With Macro and Run
#Build macro and target for console application"
command.name.6.*.n=Build With Macro and Run
command.6.*.n=build-and-run.cmd $(FileName)
command.6.subsystem.*.n=0
command.save.before.6.*.n=1
command.shortcut.6.*.n=F5
При этом, если что, нужно или изменить F5 на удобную для себя кнопку, или убрать/изменить аналогичный шорткат у имеющегся в nemerle.properties пункта.
Теперь открыв в Сцинтиле файл с расширением .n вы можете просто нажатием на F5 скомпилировать его и запустить на выполнение.
Если при этом рядом с компилируемым файлом будет лежать файл начальная часть имени которого совпадает с компилируемым, и оно оканчивается на «-macro», то сначала будет произведена компиляция этого файла. А потом, вслучае успеха будет откомплирован вайл без суффикса «–macro». Причем при его компиляции будет подключаться макросборка получившаяся на первом этапе. Например, если имеются файлы:
test-001.n
test-001-macro.n
если активизировать в Сцинтиле файл test-001.n и нажать F5, то будет сначала произвдена компиляция макросборки test-001-macro.dll (на базе файла test-001-macro.n), а потом будет произведена компиляция сборки test-001.exe (на базе файла test-001.n). Причем при компиляции test-001.exe будет использована макро-сборк полученная на первом этапе.
Если все скомпилируется, то будет произведен запуск test-001.exe. Иначе будет выданы сообщения об ошибке.
Для того, чтобы убедиться, что все работает создайте простенькую тестовую пару файлов:
Test-001.n – содержащий следующий код:
// abstract, and, array, as, base, catch, class, def, delegate, do, else, enum, event, extern, false, finally, for, foreach, fun, if, implements, in, interface, internal, lock, macro, match, module, mutable, namespace, new, null, out, override, params,
// WriteLine public private protected internal
TestMacro();
Примечание: Коментарии с лабудой нужны для того, чтобы можно было пользоваться недо-интелисенсом. Если в Сцинтиле написат первые буквы слова и нажать Ctrl+Enter, то она выдаст список слов начинающихся на эти буквы.
И файл test-001-macro.n содержащий следующий код:
using System.Console;
// abstract, and, array, as, base, catch, class, def, delegate, do, else, enum, event, extern, false, finally, for, foreach, fun, if, implements, in, interface, internal, lock, macro, match, module, mutable, namespace, new, null, out, override, params,
// WriteLine public private protected internal
macro TestMacro()
{
WriteLine("compile-time\n");
<[ WriteLine("run-time\n") ]>;
}
Теперь откройте Сцинтилой файл Test-001.n и нажмите F5.
Если в ответ вы увидите следующее:
>build-and-run.cmd test-001
--# Compiling Macro-assembly #---
---------------------------------
--- Compiling target assembly ---
---------------------------------
compile-time
--------------- OK -----------------
--Run: test-001.exe
.
run-time
>Exit code: 0
Значит вы все сделали верно и первый этап знакомства с Нэмерлом и его макросами пройдет.
Если вам сказали, что мол файл не найдет, то пропишите путь к каталогу с бинарниками Нэмерла в переменную PATH окружения.
Теперь прокомментирую, что вы видите на экране. Вывод:
---------------------------------
--- Compiling target assembly ---
---------------------------------
compile-time
Означает что началась компиляция основной сборки (сборки которую мы будем запускать на исполнение) и вызвался макрос. Который вывел информацию о себе на констоль. Надпись:
run-time
Ввел код сгенерированный макросом. Если вы теперь откроете полученный исполнимый файл Рефлектором, то увидите, что в функции Main() присутствует вызов:
Console.WriteLine("run-time\n");
а от макроса не осталось и следа. Это и есть чудесное действие макросов.
Продолжение следует.
... << RSDN@Home 1.2.0 alpha rev. 637>>
14.03.06 11:09: Перенесено из 'Философия программирования'
30.01.07 18:23: Перенесено модератором из 'Декларативное программирование' — IT
Итак, сейчас я расскажу, что такое квази-циритвание (quasi-quotation).
Квази-циритвание — это очень простая идея, но именно она делает макросы Nemerle-а мощнейшим и в тоже время относительно безопасным инструментом по сравнению с текстуальными макросами С/С++.
Прежде чем разбираться с тем, что такое квази-цитирование давайте сначала разберемся, что вообще означает термин «цитирование».
Компилятор преобразует текст программы сначала в список токенов (умные ученые мужи еще называют их терминалами), а потом в так называемое абстрактное синтаксическое дерево (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
.
Теперь настала пора понять, что же означает приставка «квази».
Изменим немного наш пример:
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>>