Продолжение о синтаксических макросах немерле.
Начало тут. Макрооператоры
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ - макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в C# никто не отменял, но в некоторых ситуациях хочется определить оператор для типов, к которым нет доступа, либо семантика оператора такова, что к какому-то конкретному типу его привязать сложно.
В качестве примера выкладываю код петуш оператора, который имитирует питоновское умножение числа на строку, то есть просто повторяет строку указанное число раз:
copy to clipboard
подсветка кода- [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:
copy to clipboard
подсветка кода- Силы (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'ом по исходникам Nemerle по фразам «OperatorAttribute» или «OperatorInfo».
При описании оператор If-Else я упомянул, что для того, чтобы парсер мог распознать вызов макроса в исходном коде, синтаксис макроса должен начинаться с константного префикса. Макрооператоры позволяют в некоторых случаях обойти это ограничение, хотя придется проделать некоторое количество ручной работы. Таким образом можно создавать тернарные операторы (а можно и n-арные).
Как-то я обнаружил, что из-за того, что F# не умеет работать с объектами типа dynamic, передача данных во View (речь про ASP.NET MVC) стала пестрить уродливым доступом к значению в словаре по его ключу. Но для F# это не было проблемой, потому что сахар валялся на поверхности. Когда я стал разбираться с Nemerle мне захотелось повторить данный прием. Вышло как-то так:
copy to clipboard
подсветка кода- [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#
copy to clipboard
подсветка кода- //Небольшое, но гордое русскоязычное комьюнити немерлистов
- //испытывает сильнейшую боль от ентой точки
- 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. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.