Entry tags:
Про синтаксические макросы в Nemerle. Часть 2
Продолжение о синтаксических макросах немерле. Начало тут.
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ — макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в C# никто не отменял, но в некоторых ситуациях хочется определить оператор для типов, к которым нет доступа, либо семантика оператора такова, что к какому-то конкретному типу его привязать сложно.
В качестве примера выкладываю кодпетуш оператора, который имитирует питоновское умножение числа на строку, то есть просто повторяет строку указанное число раз:
Существенным в этом примере, является атрибут уровня сборки
в котором описываются параметры оператора:
- пространство имен, в котором определен оператор,
- его имя (Кстати можно использовать не только ХАСКИАРТ, но любые другие символы. С таким же успехом оператор мог называться strReply)
- является ли оператор унарным (в противном случае — бинарный. КО)
- сила связывания слева
- сила связывания справа
Сила связывания задает ассоциативность оператора. Если сила слева меньше чем справа, оператор будет левоассоциативным и наоборот. Кроме того она задает приоритет оператора. Это работает неочевидным образом, по крайней мере для меня, потому я покажу это на примере для выражения "ООО"+"ЗАО" *> 3*3+4:
Думаю, понятно, что необходимо учитывать возможность взаимодействия по крайней мере с операторами из стандартной библиотеки. Посмотреть их приоритеты можно где-то здесь и здесь. Сам я пользуюсь grep'ом по исходникам Nemerle по фразам «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. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.
Макрооператоры
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ — макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в 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'ом по исходникам Nemerle по фразам «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. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.
no subject
Лепить-то ладно, все могут, вот совмещать!..
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)
(no subject)