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
Я не очень понимаю, что имеется в виду. Допустим, у меня есть есть edsl для описания роутинга, есть edsl для описания шаблонов генерации html. Что в данном случае следует считать их комбинацией? Из edsl описания шаблонов можно получать информацию о маршрутах - это пример интерфейса между edsl, как его представляю себе я. А на работе у меня формируются таможенные декларации и, если я сделаю для него edsl, даже представить себе невозможно, как и зачем его можно комбинировать с edsl'ями, про которые я писал выше. То есть не существует никакой возможности произвольно комбинировать edsl, даже безотносительно языка.
>Можно ли передать макрос параметром? Может ли какой-то код быть параметризован библиотекой с макросами?
Нет. Это макрос что-то знает о коде, а не код о макросе. Однако макрос можно параметризовывать реализацией. Например https://github.com/rsdn/nemerle/wiki/Computation-Expression-macro В данном случае, макрос comp параметрузуется бидлером, после чего сomputation expressions разворячиваются в конструкции целевого билдера. Как-то так отделяется логика от реализации.
>Может ли какой-то код быть параметризован библиотекой с макросами?
Как это "параметризован библиотекой"?
no subject
EDSL - это скрытая библиотека (и наоборот). Как это нет возможности комбинировать библиотеки?
>>Можно ли передать макрос параметром? Может ли какой-то код быть параметризован библиотекой с макросами?
>Нет.
Вот.
Мы только что выяснили слабое место макросов. Макросы не являются первоклассной сущностью.
Чем больше в ЯП первоклассных сущностей, тем лучше. Лисп чем хорош? Практически всё является первоклассной сущностью, sexp. Макросы уже потом этим пользуются. Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией. Нужда в макросах возникает сильно потом.
>Как это "параметризован библиотекой"?
В ML это называется функторы или модули, как-то так. В Хаскеле - конструкторы типов.
no subject
EDSL - это язык, оперирующий терминами предментной области. При помощий макросов их действительно возможно создать. А комбинировать библиотеки можно сколько угодно, только не стоит называть какой-нибуть sqldatareader "EDSL'ем для работы с реляционными базами".
>Практически всё является первоклассной сущностью, sexp. Макросы уже потом этим пользуются.
Абсолюдня индентичная ситуация в Nemerle. Код в виде квази-цитат является первоклассной сущностью, которой можно оперировать при помощи макросов. Я подозреваю, что отличие только в том, что лисповые макросы можно исполнять в рантайме. В комьюнити немерле возникала идея прикрутить возможность использовать макросы для динамическом компиляции сборок, но дальше идеи дело не пошло, видимо, было не очень нужно.
>Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией.
do-нотация не является первоклассной сущностью и ее нелья никуда передать. И опять же, я не могу представить себе семантику такой "передачи".
>>Как это "параметризован библиотекой"?
>В ML это называется функторы или модули, как-то так. В Хаскеле - конструкторы типов.
Это имеет отношение к системе типов и полиморфизму. В Nemerle под словом "макрос" стоит понимать расширение компилятора, которое, во-первых, добавляет новые правила в парсер, а, во-вторых, имеет доступ практически ко всем структурам, которыми оперирует компилятор (исходный код, AST, типизированное AST) и может ими оперировать для введения новых конструкций в язык. Система типов и макросистема - непротиворечивые вещи, которые можно использовать одновременно. Типы - для валидации кода из коробки и оптимизаций компилятором, макросы - для создания EDSL и кодогенерации.
no subject
Только типы 1) могут быть сущностями первого класса и 2) много мощнее макросов.
>EDSL - это язык, оперирующий терминами предментной области. При помощий макросов их действительно возможно создать.
Предметные области могут быть параметризованы. Предметная область "язык программирования" может быть параметризована предметной областью компилятор и предметной областью интерпретатор. И обработка данных в этих двух комбинациях будет разной - полный сбор и преобразование в компиляторе и частичный сбор и преобразование в интерпретаторе.
Валяйте, сделайте такое на макросах.
>>Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией.
>do-нотация не является первоклассной сущностью и ее нелья никуда передать. И опять же, я не могу представить себе семантику такой "передачи".
До изобретения do-нотации использовали >>=, >> и return. И сейчас используют. И теряют совсем немного (и часто выигрывают).
>Система типов и макросистема - непротиворечивые вещи, которые можно использовать одновременно.
А можно использовать только систему типов. См. зависимые типы данных.
no subject
Кроме того макросы позволяют снизить цикломатическую сложность кода, путем заталкивая бойлерплейта, неизбежно возницающего из-за несоответствия между сущностями предметной области и сущностями языка программирования обзщего назначения, в реализацию макроса.
>Только типы 1) могут быть сущностями первого класса
В случае с макросистемой: код - сущность первого класса, а макросы - нет
В случае с системой типов: тип - сущность первого класса, а сама система типов - нет.
Или существуют способы передавать систему и параметризовывать ею код?
На мой взгляд, ситуация идентичная, хотя я встречал утверждения, что макросы могут задавать систему типов, но я не понимаю, как это может работает.
>2) много мощнее макросов.
Я уже писал, что считаю типы ортоганальными макросам, потому не ясно на основании каких каких критериев можно сделать подобный вывод.
>До изобретения do-нотации использовали >>=, >> и return. И сейчас используют. И теряют совсем немного (и часто выигрывают).
do-нотация - это лишь пример синтаксического сахара, которого в Haskell в большом достатке. Или list comprehensions тоже не используются? При этом в Haskell они есть, но через типы их сделать нельзя.
>Предметные области могут быть параметризованы. Предметная область "язык программирования" может быть параметризована предметной областью компилятор и предметной областью интерпретатор.
Это не параметризация предментной области, а просто различные реализации. Это было бы очень неприятно, если бы в случае с компилятором у нас был бы один язык, а с интерпретатором - другой. Не смотря на то, что в Haskell сделано именно так: ghci забрасывать пользователя прямиков внутрь IO-монады, после чего пользователь наслаждается всеми прелестями do-нотации, полезность которой вы отрицаете, я считаю это плохой практикой. При этом проблемы с реализаций различного поведения на макросах быть не должно. В одном случае сайд-эффекты будут применятся по ходу процесса разбора и типизации, а в другом - после.
>А можно использовать только систему типов. См. зависимые типы данных.
Simon Peyton-Jones и компания решили, что для монад и списков нужен сахар, как и во многих других местах. Если бы системы типов было бы достаточно, сахар бы не множился. Сама реальность языка Haskell опровергает это вашей утверждение. В этом констексте, интересно было бы узнать, что именно натолкнуло разработчиков Скалы на запиливание макросов в язык.
no subject
Я не про Хаскель говорил.
no subject
Эти понятия не применимы к макросам. Ты просо рассуждаешь о том что не понимаешь.
Макросы это языковые расширения и/или метапрограммы (в зависимости от типа и решаемой задачи).
Их не надо куда-то предавать. Их параметры это произвольное AST (код без интепретации). Так что в них можно передать что угодно но в виде кода. Их передавать никуда нельзя просто потому что это бессмысленно. Код же макросов - это произвольный код на Немерле. Его можно передавать куда угодно как и любую функцию и в нем можно использовать что угодно.
no subject
Vlad, иди ты нах-й!
no subject
Ты бы не лез рассуждать о том в чем некомпетентен. Тогда и в подобные ситуации не попадал бы. ;)
А если уж лезешь, то для начала изучи вопрос обсуждения. Противно ведь слушать подобную чушь.
no subject
no subject
no subject
Запомни этот опыт. Он тебе не раз ещё пригодится.
no subject
> Мы только что выяснили слабое место макросов. Макросы не являются первоклассной сущностью.
Мы только что выяснили отсутствие у тебя компетенции по обсуждаемому вопросу. Не более того.
Макросы - это точка входа в метамир (в компилятор). А внутри это просто код к который можно обернуть функцией (лябдой, например) и передавать куда угодно. Нужно только понимать, что это метакода (работающий на стадии компиляции), так что передать его в какие-то части компилируемой программы физически невозможно.
no subject
В Лиспе маросы очень похожие. Они так же работают во время "компиляции" программы. Только динамическая природа Лиспа позволяет интерпретировать код в ратайме. В остальном отличий нет. Макрос точно так же не первоклассная сущность в твоем понимании. Это метапрограмма занимающаяся переписыванием одного AST в другое.