Про синтаксические макросы в Nemerle
Jul. 16th, 2012 11:40 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Пришло время написать макрос. Макрос сам себя не напишет.
Я тут некоторое время писал кое-какую мелочевку на Nemerle, и, можно сказать, язык пришелся мне по нраву. Очень похоже на F# хотя и своими особенностями. У меня пару раз возникало желание написать макрос, который будет заниматься небольшой кодогенерацией по месту, но, после небольшого размышления, я отказывался от этой затеи. Отчасти из-за неприязни к особым формам и неявному поведению (странно, что при этом я обожаю всяческий синтаксический сахар), отчасти из-за сложности написания самих макросов. Чтобы выяснить, что же меня больше смущает, я решил научиться их писать. В этом посте, я старался систематизировать то, что узнал о синтаксических макросах Nemerle.
Вообще-то написать простейший макрос очень просто. Необходимо создать проект, в котором будут находится макросы и добавить в него сам макрос, для этого в студии даже предусмотрен специальный визард, который позволяет выбрать тип макроса, задать некоторые специфичные особенности и параметры. После чего надо добавить ссылку на сборку к проекту, где макрос будет использован. В проекте Nemerle для этого есть два мета: References и Macro References. При разработке макроса, лучше добавлять сборку в References, тогда изменения будут перекомпилироваться на лету. В два клика можно получить примерно следующее:
Были сгенерированы макрос и метод его реализации. (Вообще-то в таком делении нет никакой необходимости, но в теле макроса не поддерживается intellisense, а это, пожалуй, однин из самых страшных кошмаров дотнетчика) Макрос принимает на вход один аргумент с типом PExpr, который представляет собой выражение Nemerle. Для демонстрации я просто добавил вывод на экран текста выражения, которое было передано в макрос, а потом и само выражение.

На картинке видно, что произойдет с кодом после раскрытия макроса. Здесь есть несколько не очевидных моментов, которые надо пояснить.
Во-первых, exrp не является текстом, как это может показаться. Это сложный объект, содержащий много разного, вроде позиции, на которой выражение находится в исходном коде, а также позволяющий определить тип выражения. В данном случае можно опустить декларацию типа в описании макроса, потому что тип PExpr принят по умолчанию. Для указания типов параметров макроса возможно использовать типы из следующего списка: PExpr, Token, params array[PExpr], params list[PExpr], bool, byte, decimal, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
Надо понимать, что требование любого типа кроме первых четырех, означает, что макрос будет принимать на вход константу времени компиляции, которыми могут являться только литералы: строковые для string, булевы для bool, и числовые для остальных. Например для макроса, ожидающего long, 10l будет корректным параметром, а long.Parse("10l") - уже нет, поскольку это выражение и его тип PExpr.
Во-вторых, выражение в адских скобках <[ ]> попадет в код целевой программы после работы макроса.
В-третьих, оператор $ означает, что в код целевой программы попадет значение, которое содержится в переменной, идущей следом.
Макросистема Nemerle использует механизм квази-цитирования, то есть работа идет не с текстом, а с куском AST (абстрактное синтаксическое дерево), в которое будет разобран данный фрагмент кода при компиляции. Тип PExpr как раз представляет собой квази-цитату. Квази-цитату можно формироваться при помощи <[ ]>, можно достать из нее значение, используя $, можно узнать тип выражения, которое она содержит, можно разбирать паттерн-матчингом. Я переписал код макроса foo следующим образом, чтобы макроугар прямо в глаза:
И при компиляции его вызов foo(WriteLine("lol")), будет заменен на
Вообще-то, я хотел выделить эти два фрагмента кода каким-нибудь клоунским шрифтом вроде comic sans, написать «тут все понятно» и двигаться дальше, поскольку любые мои попытки сформулировать, что здесь происходит скатывались в
Ехало выражение через выражение,
Видит выражение в выражении выражение
Выражение выражение выражение.
Но я все же постараюсь. Грубо говоря, внутри квази-цитаты существует своя область видимости, куда не входят выражение из вне. Потому Console.WriteLine(expr) может иметь доступ только к expr, определенному внутри <[ ]>. А если удалить строку def expr = "trololo", то компилятор ругнется, мол Unbound name exrp. В то же время при использовании $ вставляется _значение_ переменной, следующей за ним, а не ее имя или что-либо еще.
Может возникнуть вопрос, что произойдет, если в коде целевой программы уже используется имя expr. Если с тем же самым макросом foo, написать
на экран будет выведено
то есть никаких конфликтов имен или неожиданного поведения не наблюдается. Это возможно благодаря тому, что макросы Nemerle гигиеничны. Это значит, что при конфликте имен, переменные внутри макроса будут переименованы.
Вызов макроса foo был неотличим от обычного вызова функции, но Nemerle позволяет при помощи макросов вводить новый синтаксис. Я буду велосипедить оператор If-Else, потому что он похож на Hello World из мира макросов. Вот его код:
Теперь у меня есть условный оператор, очень похожий на тот, что используется F#:
Синтаксис задается конструкцией syntax, в ней поочередно указывается из чего состоит вывоз макроса. На мой взгляд, здесь все достаточно прозрачно. Механизм вывода типов Nemerle распространяется в том числе и на макросы, потому компилятор, увидев сопоставление значения cond с true, определит, что значение cond должно иметь тип bool и будет требовать его от пользователя, (хотя и не самым пристойным образом). Указанный синтаксис используется парсером для выявления вызова макроса, а специфика его (парсера) работы такова, что общем случае могут быть распознаны только конструкции имеющие определенный префикс, которым в данном случае являет "If". Иными словами, не стоит начинать описание синтаксиса макроса с переменной, а надо первым делом ставить строку.
В предыдущих примерах я не использовал информацию о типах выражений, которые передаются на вход макросу. Однако возможность типизировать квази-цитату является одним из главных достоинств макросов Nemerle. Имея информацию о типах во время компиляции, можно реализовывать различные проверки или генерировать код в зависимости от типа полученного выражения. Например, функция printf выполняет проверку типов своих аргументов на этапе компиляции и встроена во многие языки программирования. Это нормальное, но не всегда самое лучшее решении. Правила формирования строк специфичные для конкретной задачи могут быть весьма сложны, потому иметь смысл автоматизировать эти проверки и выполнять их во время компиляции.
Для примера я решил сделать ту самую функцию printf. Вообще-то макрос с таким функционал реализован в одной из статей по Nemerle, но он содержит огромное количество имеративной ёбы, потому мне было сложно выделить моменты характерные для написания макросов. Вот мой игрушечный пример prinf'а (хотя я не вижу проблемы в расширении его до полноценной реализации):
Я надеюсь, что комментариев достаточно для понимая как это работает в общем виде. Основной интерес здесь представляет первый аргумент typer : Typer функции DoTransform. Именно с его помощью возможно получение доступа к типу выражения, его проверка, уточнение или фиксация. Важно, что в выражении def argType = typer.TypeExpr(arg).Type; argType имеет тип TypeVar, а не родной дотнетовский Type, как могло показаться. TypeVar – это тип, которым оперирует компилятор Nemerle в процессе типизации. Он может быть фиксированным так и неконкретным. В первом случае можно использовать FixedType, который является наследником TypeVar. Список доступной информации достаточно большой. Ниже мемберы TypeVar'а
Методы: _N_GetVariantCode, CheckAccessibility, CompareTo, ConstructSubstForTypeInfo, DeepFix, Equals, EqualsUpperBound, Expand, Fix, Fixate, ForceProvide, ForceRequire, ForceUnify, FunParamsTypes, FunReturnTypeAndParms, GetFunctionArguments, GetHashCode, GetInstantiatedSuperType, GetNonVoidSystemType, GetSystemType, GetType, GetUnfixedFunctionArguments, IsAccessibleFrom, Iter, Provide, Require, SetParsedObject, SigRequire, ToList, ToString, TryEnforcingEquality, TryProvide, TryRequire, TrySigRequire, TryUnify, TypeOfMember, TypeOfMethod, TypeOfMethodWithTyparms, Unify, UpdateRelatedParsedObject
Свойства: AlwaysUnify, AnyHint, CanBeNull, CurrentSolver, FixedValue, Hint, HintFast, ImplicitCastHint, InternalType, IsConstrained, IsEnum, IsFixed, IsFree, IsFresh, IsFromNull, IsFunction, IsGenerated, IsInterface, IsNullable, IsPrimitive, IsSeparated, IsSystemObject, IsValueType, Location, LowerBound, ParsedObject, SystemTypeCache, TypeInfo, UpperBound
Например метод Require сообщает компилятору, какой требуется тип для данного выражения, а метод GetSystemType получается тот самый дотнетовский тип, но им надо пользоваться осторожно, поскольку в момент его вызова тип выражения фиксируется и не может в дальнейшем быть изменен.
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ — макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в C# никто не отменял, но в некоторых ситуациях хочется определить оператор для типов, к которым нет доступа, либо семантика оператора такова, что к какому-то конкретному типу его привязать сложно.
В качестве примера выкладываю кодпетуш оператора, которыми имитирует питоновское умножение числа на строку, то есть просто повторяет строку указанное число раз:
Существенным в этом примере, является атрибут уровня сборки
в котором описывают параметры оператора:
- пространство имен, в котором определен оператор,
- его имя (Кстати можно использовать не только ХАСКИ, но любые другие символы. С таким же успехом оператор мог называться strReply)
- является ли оператор унарным (в противном случае — бинарный. КО)
- сила связывания слева
- сила связывания справа
Сила связывания задает ассоциативность оператора. Если сила слева меньше чем справа, оператор будет левоассоциативным и наоборот. Кроме того она задает приоритет оператора. Это работает очевидно, по крайней мере для меня, потому я покажу это на примере для выражения "ООО"+"ЗАО" *> 3*3+4:
Думаю, понятно, чтонеобходимо учитывать возможность взаимодействия по крайней мере с операторами из стандартной библиотеки. Посмотреть их приоритеты можно где-то здесь и здесь. Сам я пользуюсь grep'ом по исходникам по фразам «OperatorAttribute» или «OperatorInfo».
При описании оператор If-Else я упомянул, что для того, чтобы парсер мог распознать вызов макроса в исходном коде, синтаксис макроса должен начинаться с константного префикса. Макрооператоры позволяют в некоторых случаях обойти это ограничение, хотя придется проделать некоторое количество ручной работы. Таким образом можно создавать тернарные операторы (а можно и n-арные).
Как-то я обнаружил, что из-за того, что F# не умеет работать с объектами типа dynamic, передача данных во View (речь про ASP.NET MVC) стала пестрить уродливым доступом к значению в словаре по его ключу. Но для F# это не было проблемой, потому что сахар валялся на поверхности. Когда я стал разбираться с Nemerle мне захотелось повторить данный прием. Вышло как-то так:
Мой оператор умеет не только добавлять пару ключ-значение в словарь, но и обновлять значение, если ключ уже существует, а также возвращать значение по ключу, если пользователь ничего присваивать не стал. При этом работа с таким оператором, мало отличается от работы с родным dynamic из C#
Самое интересное в этом примере — это сопоставление квази-цитаты с образцом, которое позволяет достаточно удобным способом выделять из квази-цитаты составные части. Возможность формировать цитату из фрагментов внешнего AST или объектов иного типа, а также разбирать квази-цитату на составляющие, называется сплайсингом. Подробнее о сплайсах в Nemerle можно почитать здесь.
Что-то вроде заключения
Хотя мой макрооператор @? может показаться бесполезным, он демонстрирует, как макросы изменять семантику языка, вводя в нее новый конструкции. Они позволяют на базе простого класса вроде public class YobaDynamic : Dictionary[string, object] { } реализовать половинупетуш Питона, при желании.
Я постарался максимально компактным образом описать то, что узнал про синтаксические макросы Nemerle. Как можно видеть, писать их относительно несложно. Сложнее продумать логику работы макроса и его взаимодействие с внешним миром (например макросы Nemerle не поддерживают перегрузку, и можно нечаянно что-то сломать, особенно это просто сделать макрооператорами). Макросы могут быть вредны, поскольку то, что кажется красивым, логичным и читаемым для одного человека, может выглядеть как сраный ХАСКИАРТ для другого. В то же время, такие вещи как синтаксис LINQ в C# или do-нотация в Haskell можназделость с помощью этих самых макросов. Это позволяет поднять читаемость и разделить предметную область и реализацию. Это имеет непосредственное отношение к созданию DSL, то есть решение задачи в терминах предметной области без привлечения неведомой ёбы вроде СЕРИАЛИЗАТОРОВ или там РЕГИСТРОВ ПРОЦЕССОРА, как в каменном веке. В тоже время придумать полноценный DSL (SQL, HTML и так далее), который на 100% покроет все возможные юзкейсы — задача практически невыполнимая. И разумным выходом выглядит создание eDSL (e – embeded, то есть встроенных в язык). Эту идею я вычитал вот в этом посте. В данном контексте Nemerle представляется достаточно удобным инструментом, поскольку позволяет на ура лепить эти самые eDSL'и.
Синтаксическим макросами возможности макросистемы Nemerle не ограничиваются. Существуют также макроатрибуты применимые к следующим объектам: Class, Method, Field, Property, Event, Parameter, Assembly. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.
Я тут некоторое время писал кое-какую мелочевку на Nemerle, и, можно сказать, язык пришелся мне по нраву. Очень похоже на F# хотя и своими особенностями. У меня пару раз возникало желание написать макрос, который будет заниматься небольшой кодогенерацией по месту, но, после небольшого размышления, я отказывался от этой затеи. Отчасти из-за неприязни к особым формам и неявному поведению (странно, что при этом я обожаю всяческий синтаксический сахар), отчасти из-за сложности написания самих макросов. Чтобы выяснить, что же меня больше смущает, я решил научиться их писать. В этом посте, я старался систематизировать то, что узнал о синтаксических макросах Nemerle.
Вообще-то написать простейший макрос очень просто. Необходимо создать проект, в котором будут находится макросы и добавить в него сам макрос, для этого в студии даже предусмотрен специальный визард, который позволяет выбрать тип макроса, задать некоторые специфичные особенности и параметры. После чего надо добавить ссылку на сборку к проекту, где макрос будет использован. В проекте Nemerle для этого есть два мета: References и Macro References. При разработке макроса, лучше добавлять сборку в References, тогда изменения будут перекомпилироваться на лету. В два клика можно получить примерно следующее:
- macro foo(expr: PExpr) {
- fooImpl.DoTransform(Macros.ImplicitCTX(), expr)
- }
- module fooImpl {
- public DoTransform(_ : Typer, expr : PExpr) : PExpr {
- <[ Console.WriteLine($(expr.ToString()));
- $expr ]>
- }
- }
Были сгенерированы макрос и метод его реализации. (Вообще-то в таком делении нет никакой необходимости, но в теле макроса не поддерживается intellisense, а это, пожалуй, однин из самых страшных кошмаров дотнетчика) Макрос принимает на вход один аргумент с типом PExpr, который представляет собой выражение Nemerle. Для демонстрации я просто добавил вывод на экран текста выражения, которое было передано в макрос, а потом и само выражение.

На картинке видно, что произойдет с кодом после раскрытия макроса. Здесь есть несколько не очевидных моментов, которые надо пояснить.
Во-первых, exrp не является текстом, как это может показаться. Это сложный объект, содержащий много разного, вроде позиции, на которой выражение находится в исходном коде, а также позволяющий определить тип выражения. В данном случае можно опустить декларацию типа в описании макроса, потому что тип PExpr принят по умолчанию. Для указания типов параметров макроса возможно использовать типы из следующего списка: PExpr, Token, params array[PExpr], params list[PExpr], bool, byte, decimal, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
Надо понимать, что требование любого типа кроме первых четырех, означает, что макрос будет принимать на вход константу времени компиляции, которыми могут являться только литералы: строковые для string, булевы для bool, и числовые для остальных. Например для макроса, ожидающего long, 10l будет корректным параметром, а long.Parse("10l") - уже нет, поскольку это выражение и его тип PExpr.
Во-вторых, выражение в адских скобках <[ ]> попадет в код целевой программы после работы макроса.
В-третьих, оператор $ означает, что в код целевой программы попадет значение, которое содержится в переменной, идущей следом.
Квази-цитирование и гигиеничность
Макросистема Nemerle использует механизм квази-цитирования, то есть работа идет не с текстом, а с куском AST (абстрактное синтаксическое дерево), в которое будет разобран данный фрагмент кода при компиляции. Тип PExpr как раз представляет собой квази-цитату. Квази-цитату можно формироваться при помощи <[ ]>, можно достать из нее значение, используя $, можно узнать тип выражения, которое она содержит, можно разбирать паттерн-матчингом. Я переписал код макроса foo следующим образом
- public DoTransform(_ : Typer, expr : PExpr) : PExpr {
- <[ def expr = "trololo";
- Console.WriteLine(expr);
- $expr;
- Console.WriteLine($(expr.ToString())); ]>
- }
И при компиляции его вызов foo(WriteLine("lol")), будет заменен на
- {
- def expr = "trololo";
- Console.WriteLine(expr);
- WriteLine("lol");
- Console.WriteLine("WriteLine(\"lol\")")
- }
Вообще-то, я хотел выделить эти два фрагмента кода каким-нибудь клоунским шрифтом вроде comic sans, написать «тут все понятно» и двигаться дальше, поскольку любые мои попытки сформулировать, что здесь происходит скатывались в
Ехало выражение через выражение,
Видит выражение в выражении выражение
Выражение выражение выражение.
Но я все же постараюсь. Грубо говоря, внутри квази-цитаты существует своя область видимости, куда не входят выражение из вне. Потому Console.WriteLine(expr) может иметь доступ только к expr, определенному внутри <[ ]>. А если удалить строку def expr = "trololo", то компилятор ругнется, мол Unbound name exrp. В то же время при использовании $ вставляется _значение_ переменной, следующей за ним, а не ее имя или что-либо еще.
Может возникнуть вопрос, что произойдет, если в коде целевой программы уже используется имя expr. Если с тем же самым макросом foo, написать
- def expr = "PSHH PSHHHH";
- foo(WriteLine("lol"));
- WriteLine(expr);
на экран будет выведено
- trololo
- lol
- WriteLine("lol")
- PSHH PSHHHH
то есть никаких конфликтов имен или неожиданного поведения не наблюдается. Это возможно благодаря тому, что макросы Nemerle гигиеничны. Это значит, что при конфликте имен, переменные внутри макроса будут переименованы.
Новый синтаксис
Вызов макроса foo был неотличим от обычного вызова функции, но Nemerle позволяет при помощи макросов вводить новый синтаксис. Я буду велосипедить оператор If-Else, потому что он похож на Hello World из мира макросов. Вот его код:
- macro IfThenElse(cond, e1, e2)
- syntax ("If", cond, "Then", e1, "Else", e2) {
- IfThenElseImpl.DoTransform(Macros.ImplicitCTX(), cond, e1, e2)
- }
- module IfThenElseImpl {
- public DoTransform(_ : Typer, cond : PExpr, e1 : PExpr, e2 : PExpr) : PExpr {
- <[ match($cond){
- | true => $e1;
- | _ => $e2;
- } ]>
- }
- }
Теперь у меня есть условный оператор, очень похожий на тот, что используется F#:
- def x = If ReadLine().Length==3 Then "lol" Else "fail";
- dLine().Length==3 Then Write("lol") Else Write("fail");
Синтаксис задается конструкцией syntax, в ней поочередно указывается из чего состоит вывоз макроса. На мой взгляд, здесь все достаточно прозрачно. Механизм вывода типов Nemerle распространяется в том числе и на макросы, потому компилятор, увидев сопоставление значения cond с true, определит, что значение cond должно иметь тип bool и будет требовать его от пользователя, (хотя и не самым пристойным образом). Указанный синтаксис используется парсером для выявления вызова макроса, а специфика его (парсера) работы такова, что общем случае могут быть распознаны только конструкции имеющие определенный префикс, которым в данном случае являет "If". Иными словами, не стоит начинать описание синтаксиса макроса с переменной, а надо первым делом ставить строку.
Типизация
В предыдущих примерах я не использовал информацию о типах выражений, которые передаются на вход макросу. Однако возможность типизировать квази-цитату является одним из главных достоинств макросов Nemerle. Имея информацию о типах во время компиляции, можно реализовывать различные проверки или генерировать код в зависимости от типа полученного выражения. Например, функция printf выполняет проверку типов своих аргументов на этапе компиляции и встроена во многие языки программирования. Это нормальное, но не всегда самое лучшее решении. Правила формирования строк специфичные для конкретной задачи могут быть весьма сложны, потому иметь смысл автоматизировать эти проверки и выполнять их во время компиляции.
Для примера я решил сделать ту самую функцию printf. Вообще-то макрос с таким функционал реализован в одной из статей по Nemerle, но он содержит огромное количество имеративной ёбы, потому мне было сложно выделить моменты характерные для написания макросов. Вот мой игрушечный пример prinf'а (хотя я не вижу проблемы в расширении его до полноценной реализации):
- macro MyPrintf(formatString : string, params args : array[PExpr]){
- MyPrintfMacroImpl.DoTransform(Macros.ImplicitCTX(), formatString, args)
- }
- module MyPrintfMacroImpl {
- //словари соответсвий формат - допустимые типы
- _patternTypes : Dictionary[string, list[PExpr]] = Dictionary() <- [
- "%d" = [<[ int ]>, <[ short ]>],
- "%s" = [<[ string ]>],
- "%f" = [<[ bool ]>],
- "%time"= [<[ DateTime ]>]
- ];
- //автомаичеси собираемая регулярочка, для выделения строк формата
- _patternRegex : Regex = Regex(string.Format("({0})", string.Join("|", _patternTypes.Keys)));
- public DoTransform(typer : Typer, formatExpr : string, args : array[PExpr]) : PExpr {
- //Инциализуем тайпер
- Macros.DefineCTX(typer);
- //Разбиваем строку формата на фрагменты и нумеруем полученные фрагменты
- def parts = _patternRegex.Split(formatExpr).Select((s,i) => (i, s)).NToList();
- //Отделяем обычные строки от строк форматирования
- def(patterns, strings) = parts.Partition((_,p) => _patternRegex.IsMatch(p));
- //Бросаем ошибку, если количество строк форматирования отличается от количества аргументов
- when(patterns.Length != args.Length)
- Message.Error($"MyPrintf expected $(patterns.Length) arguments, got $(args.Length)");
- //Превращаем строки в выражения (строковые константы)
- def exprStr = strings.Map((i,s) => (i,<[ $s ]>));
- //Выполняем подстановку аргументов, вместо строк формата, проверяя соответствие типов
- def exprVal = patterns.Map2(args, fun((i, p), arg) {
- //Вычисляем тип аргумета
- def argType = typer.TypeExpr(arg).Type;
- //Если тип не соответствует строке форматирования, бросаем ошибку
- when(_patternTypes[p].All(t=> typer.BindType(t) |> argType.TryRequire |> ! _))
- Message.Error(arg.Location,
- $"Argument #$i type $(argType) is not compatible for $p, Allowed types: $(_patternTypes[p])");
- (i, arg)
- });
- //Соединяем выражения для обычных строк и строк-форматирования
- //Упорядочиваем их по номеру
- //Избавляемся от номера, выбирая только выражения
- def exrpSeq = exprStr.Concat(exprVal).OrderBy((i,_)=>i).Map((_, e) => e);
- //Формируем результирующий код
- <[string.Concat(..$exrpSeq) |> Console.WriteLine]>
- }
- }
Я надеюсь, что комментариев достаточно для понимая как это работает в общем виде. Основной интерес здесь представляет первый аргумент typer : Typer функции DoTransform. Именно с его помощью возможно получение доступа к типу выражения, его проверка, уточнение или фиксация. Важно, что в выражении def argType = typer.TypeExpr(arg).Type; argType имеет тип TypeVar, а не родной дотнетовский Type, как могло показаться. TypeVar – это тип, которым оперирует компилятор Nemerle в процессе типизации. Он может быть фиксированным так и неконкретным. В первом случае можно использовать FixedType, который является наследником TypeVar. Список доступной информации достаточно большой. Ниже мемберы TypeVar'а
Методы: _N_GetVariantCode, CheckAccessibility, CompareTo, ConstructSubstForTypeInfo, DeepFix, Equals, EqualsUpperBound, Expand, Fix, Fixate, ForceProvide, ForceRequire, ForceUnify, FunParamsTypes, FunReturnTypeAndParms, GetFunctionArguments, GetHashCode, GetInstantiatedSuperType, GetNonVoidSystemType, GetSystemType, GetType, GetUnfixedFunctionArguments, IsAccessibleFrom, Iter, Provide, Require, SetParsedObject, SigRequire, ToList, ToString, TryEnforcingEquality, TryProvide, TryRequire, TrySigRequire, TryUnify, TypeOfMember, TypeOfMethod, TypeOfMethodWithTyparms, Unify, UpdateRelatedParsedObject
Свойства: AlwaysUnify, AnyHint, CanBeNull, CurrentSolver, FixedValue, Hint, HintFast, ImplicitCastHint, InternalType, IsConstrained, IsEnum, IsFixed, IsFree, IsFresh, IsFromNull, IsFunction, IsGenerated, IsInterface, IsNullable, IsPrimitive, IsSeparated, IsSystemObject, IsValueType, Location, LowerBound, ParsedObject, SystemTypeCache, TypeInfo, UpperBound
Например метод Require сообщает компилятору, какой требуется тип для данного выражения, а метод GetSystemType получается тот самый дотнетовский тип, но им надо пользоваться осторожно, поскольку в момент его вызова тип выражения фиксируется и не может в дальнейшем быть изменен.
Макрооператоры
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ — макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в C# никто не отменял, но в некоторых ситуациях хочется определить оператор для типов, к которым нет доступа, либо семантика оператора такова, что к какому-то конкретному типу его привязать сложно.
В качестве примера выкладываю код
- [assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "*>", false, 259, 259)]
- macro @*>(str : PExpr, mult : PExpr) {
- StringMultImpl.DoTransform(Macros.ImplicitCTX(), str, mult)
- }
- module StringMultImpl {
- public DoTransform(typer : Typer, str : PExpr, mul : PExpr) : PExpr {
- Macros.DefineCTX(typer);
- def strType = typer.TypeExpr(str).Type;
- when(!strType.TryRequire(typer.BindType(<[string]>)))
- Message.Error(str.Location, $"Operator *> left argument requared string, got $strType");
- def mulType = typer.TypeExpr(mul).Type;
- when(!mulType.TryRequire(typer.BindType(<[int]>)))
- Message.Error(str.Location, $"Operator *> rigth argument requared int, got $mulType");
- <[string.Join("", NList.Repeat($str, $mul));]>
- }
- }
Существенным в этом примере, является атрибут уровня сборки
[assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "*>", false, 160, 161)]
в котором описывают параметры оператора:
- пространство имен, в котором определен оператор,
- его имя (Кстати можно использовать не только ХАСКИ, но любые другие символы. С таким же успехом оператор мог называться strReply)
- является ли оператор унарным (в противном случае — бинарный. КО)
- сила связывания слева
- сила связывания справа
Сила связывания задает ассоциативность оператора. Если сила слева меньше чем справа, оператор будет левоассоциативным и наоборот. Кроме того она задает приоритет оператора. Это работает очевидно, по крайней мере для меня, потому я покажу это на примере для выражения "ООО"+"ЗАО" *> 3*3+4:
- Силы (300, 300) → "ООО" + string.Join("", NList.Repeat("ЗАО", 3)) * 3 + 4
- Силы (260, 260) → "ООО" + string.Join("", NList.Repeat("ЗАО", 3 * 3)) + 4
- Силы (239, 239) → string.Join("", NList.Repeat("ООО" + "ЗАО", 3 * 3 + 4))
- Силы (239, 240) → string.Join("", NList.Repeat("ООО" + "ЗАО", 3 * 3)) + 4
- Cилы (242, 239) → "ООО" + string.Join("", NList.Repeat("ЗАО", 3 * 3 + 4))
Думаю, понятно, чтонеобходимо учитывать возможность взаимодействия по крайней мере с операторами из стандартной библиотеки. Посмотреть их приоритеты можно где-то здесь и здесь. Сам я пользуюсь grep'ом по исходникам по фразам «OperatorAttribute» или «OperatorInfo».
При описании оператор If-Else я упомянул, что для того, чтобы парсер мог распознать вызов макроса в исходном коде, синтаксис макроса должен начинаться с константного префикса. Макрооператоры позволяют в некоторых случаях обойти это ограничение, хотя придется проделать некоторое количество ручной работы. Таким образом можно создавать тернарные операторы (а можно и n-арные).
Как-то я обнаружил, что из-за того, что F# не умеет работать с объектами типа dynamic, передача данных во View (речь про ASP.NET MVC) стала пестрить уродливым доступом к значению в словаре по его ключу. Но для F# это не было проблемой, потому что сахар валялся на поверхности. Когда я стал разбираться с Nemerle мне захотелось повторить данный прием. Вышло как-то так:
- [assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "?", false, 142, 139)]
- macro @?(dynObj, expr) {
- DynamicAssignmenImpl.DoTransform(Macros.ImplicitCTX(), dynObj, expr)
- }
- module DynamicAssignmenImpl{
- public DoTransform(typer : Typer, dynObj: PExpr, expr: PExpr) : PExpr {
- Macros.DefineCTX(typer);
- def dynObjType = typer.TypeExpr(dynObj);
- when(! dynObjType.Type.TryRequire(typer.BindType(<[ IDictionary[string, object] ]>))) {
- def msg = $"Required IDictionary[string, object], but got $(dynObjType.Type)";
- Message.Error(dynObj.Location, msg);
- }
- match(expr) {
- | <[$(prop: name) = $e]> =>
- def key = prop.ToString();
- <[ if($dynObj.ContainsKey($key))
- $dynObj[$key] = $e;
- else
- $dynObj.Add($key, $e); ]>
- | <[$(prop: name)]> =>
- <[ $dynObj[$(prop.ToString())] ]>;
- | <[$e]> =>
- def msg = $"Expected syntx dyn?PropertyName or dyn?PropertyName = value, got $e";
- Message.Error(expr.Location, msg);
- <[()]>
- }
- }
- }
Мой оператор умеет не только добавлять пару ключ-значение в словарь, но и обновлять значение, если ключ уже существует, а также возвращать значение по ключу, если пользователь ничего присваивать не стал. При этом работа с таким оператором, мало отличается от работы с родным dynamic из C#
- //Небольшое, но гордое русскоязычное комьюнити немерлистов
- //испытывает сильнейшую боль ентой точки
- def dict = Dictionary.[string, object]();
- dict?LOL = 9+8;
- dict?YOBA = DateTime.Now;
- WriteLine($"LOL = $(dict?LOL); YOBA = $(dict?YOBA)");
- dict?YOBA = "Another YOBA";
- WriteLine($"LOL = $(dict?LOL); YOBA = $(dict?YOBA)");
Самое интересное в этом примере — это сопоставление квази-цитаты с образцом, которое позволяет достаточно удобным способом выделять из квази-цитаты составные части. Возможность формировать цитату из фрагментов внешнего AST или объектов иного типа, а также разбирать квази-цитату на составляющие, называется сплайсингом. Подробнее о сплайсах в Nemerle можно почитать здесь.
Что-то вроде заключения первой части
Хотя мой макрооператор @? может показаться бесполезным, он демонстрирует, как макросы изменять семантику языка, вводя в нее новый конструкции. Они позволяют на базе простого класса вроде public class YobaDynamic : Dictionary[string, object] { } реализовать половину
Я постарался максимально компактным образом описать то, что узнал про синтаксические макросы Nemerle. Как можно видеть, писать их относительно несложно. Сложнее продумать логику работы макроса и его взаимодействие с внешним миром (например макросы Nemerle не поддерживают перегрузку, и можно нечаянно что-то сломать, особенно это просто сделать макрооператорами). Макросы могут быть вредны, поскольку то, что кажется красивым, логичным и читаемым для одного человека, может выглядеть как сраный ХАСКИАРТ для другого. В то же время, такие вещи как синтаксис LINQ в C# или do-нотация в Haskell можназделость с помощью этих самых макросов. Это позволяет поднять читаемость и разделить предметную область и реализацию. Это имеет непосредственное отношение к созданию DSL, то есть решение задачи в терминах предметной области без привлечения неведомой ёбы вроде СЕРИАЛИЗАТОРОВ или там РЕГИСТРОВ ПРОЦЕССОРА, как в каменном веке. В тоже время придумать полноценный DSL (SQL, HTML и так далее), который на 100% покроет все возможные юзкейсы — задача практически невыполнимая. И разумным выходом выглядит создание eDSL (e – embeded, то есть встроенных в язык). Эту идею я вычитал вот в этом посте. В данном контексте Nemerle представляется достаточно удобным инструментом, поскольку позволяет на ура лепить эти самые eDSL'и.
Синтаксическим макросами возможности макросистемы Nemerle не ограничиваются. Существуют также макроатрибуты применимые к следующим объектам: Class, Method, Field, Property, Event, Parameter, Assembly. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.