![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Продолжение о синтаксических макросах немерле. Начало тут.
Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ — макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в 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)
Date: 2012-07-16 09:29 am (UTC)Лепить-то ладно, все могут, вот совмещать!..
(no subject)
Date: 2012-07-16 09:37 am (UTC)1) eDSL'и лежат в отдельных библиотеках и открываются по требованию
2) все работает в рамках одной системы типов Nemerle, потому имеется возможность создавать интерфесы для передачи данных между ентими eDSL'ями.
На данный момент, плохо, что нет перегрузки макросов, как и нет сообщений от компелятора о перекрытии одного макроса другим, особенно заметно это для макрооператоров.
(no subject)
Date: 2012-07-16 09:45 am (UTC)Простой пример. Можно ли передать макрос параметром? Может ли какой-то код быть параметризован библиотекой с макросами?
(no subject)
Date: 2012-07-16 10:04 am (UTC)Я не очень понимаю, что имеется в виду. Допустим, у меня есть есть edsl для описания роутинга, есть edsl для описания шаблонов генерации html. Что в данном случае следует считать их комбинацией? Из edsl описания шаблонов можно получать информацию о маршрутах - это пример интерфейса между edsl, как его представляю себе я. А на работе у меня формируются таможенные декларации и, если я сделаю для него edsl, даже представить себе невозможно, как и зачем его можно комбинировать с edsl'ями, про которые я писал выше. То есть не существует никакой возможности произвольно комбинировать edsl, даже безотносительно языка.
>Можно ли передать макрос параметром? Может ли какой-то код быть параметризован библиотекой с макросами?
Нет. Это макрос что-то знает о коде, а не код о макросе. Однако макрос можно параметризовывать реализацией. Например https://github.com/rsdn/nemerle/wiki/Computation-Expression-macro В данном случае, макрос comp параметрузуется бидлером, после чего сomputation expressions разворячиваются в конструкции целевого билдера. Как-то так отделяется логика от реализации.
>Может ли какой-то код быть параметризован библиотекой с макросами?
Как это "параметризован библиотекой"?
(no subject)
Date: 2012-07-16 10:26 am (UTC)EDSL - это скрытая библиотека (и наоборот). Как это нет возможности комбинировать библиотеки?
>>Можно ли передать макрос параметром? Может ли какой-то код быть параметризован библиотекой с макросами?
>Нет.
Вот.
Мы только что выяснили слабое место макросов. Макросы не являются первоклассной сущностью.
Чем больше в ЯП первоклассных сущностей, тем лучше. Лисп чем хорош? Практически всё является первоклассной сущностью, sexp. Макросы уже потом этим пользуются. Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией. Нужда в макросах возникает сильно потом.
>Как это "параметризован библиотекой"?
В ML это называется функторы или модули, как-то так. В Хаскеле - конструкторы типов.
(no subject)
Date: 2012-07-16 11:32 am (UTC)EDSL - это язык, оперирующий терминами предментной области. При помощий макросов их действительно возможно создать. А комбинировать библиотеки можно сколько угодно, только не стоит называть какой-нибуть sqldatareader "EDSL'ем для работы с реляционными базами".
>Практически всё является первоклассной сущностью, sexp. Макросы уже потом этим пользуются.
Абсолюдня индентичная ситуация в Nemerle. Код в виде квази-цитат является первоклассной сущностью, которой можно оперировать при помощи макросов. Я подозреваю, что отличие только в том, что лисповые макросы можно исполнять в рантайме. В комьюнити немерле возникала идея прикрутить возможность использовать макросы для динамическом компиляции сборок, но дальше идеи дело не пошло, видимо, было не очень нужно.
>Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией.
do-нотация не является первоклассной сущностью и ее нелья никуда передать. И опять же, я не могу представить себе семантику такой "передачи".
>>Как это "параметризован библиотекой"?
>В ML это называется функторы или модули, как-то так. В Хаскеле - конструкторы типов.
Это имеет отношение к системе типов и полиморфизму. В Nemerle под словом "макрос" стоит понимать расширение компилятора, которое, во-первых, добавляет новые правила в парсер, а, во-вторых, имеет доступ практически ко всем структурам, которыми оперирует компилятор (исходный код, AST, типизированное AST) и может ими оперировать для введения новых конструкций в язык. Система типов и макросистема - непротиворечивые вещи, которые можно использовать одновременно. Типы - для валидации кода из коробки и оптимизаций компилятором, макросы - для создания EDSL и кодогенерации.
(no subject)
Date: 2012-07-16 02:29 pm (UTC)Только типы 1) могут быть сущностями первого класса и 2) много мощнее макросов.
>EDSL - это язык, оперирующий терминами предментной области. При помощий макросов их действительно возможно создать.
Предметные области могут быть параметризованы. Предметная область "язык программирования" может быть параметризована предметной областью компилятор и предметной областью интерпретатор. И обработка данных в этих двух комбинациях будет разной - полный сбор и преобразование в компиляторе и частичный сбор и преобразование в интерпретаторе.
Валяйте, сделайте такое на макросах.
>>Хаскель чем хорош? Практически всё является первоклассной сущностью, функцией.
>do-нотация не является первоклассной сущностью и ее нелья никуда передать. И опять же, я не могу представить себе семантику такой "передачи".
До изобретения do-нотации использовали >>=, >> и return. И сейчас используют. И теряют совсем немного (и часто выигрывают).
>Система типов и макросистема - непротиворечивые вещи, которые можно использовать одновременно.
А можно использовать только систему типов. См. зависимые типы данных.
(no subject)
Date: 2012-07-16 03:22 pm (UTC)Кроме того макросы позволяют снизить цикломатическую сложность кода, путем заталкивая бойлерплейта, неизбежно возницающего из-за несоответствия между сущностями предметной области и сущностями языка программирования обзщего назначения, в реализацию макроса.
>Только типы 1) могут быть сущностями первого класса
В случае с макросистемой: код - сущность первого класса, а макросы - нет
В случае с системой типов: тип - сущность первого класса, а сама система типов - нет.
Или существуют способы передавать систему и параметризовывать ею код?
На мой взгляд, ситуация идентичная, хотя я встречал утверждения, что макросы могут задавать систему типов, но я не понимаю, как это может работает.
>2) много мощнее макросов.
Я уже писал, что считаю типы ортоганальными макросам, потому не ясно на основании каких каких критериев можно сделать подобный вывод.
>До изобретения do-нотации использовали >>=, >> и return. И сейчас используют. И теряют совсем немного (и часто выигрывают).
do-нотация - это лишь пример синтаксического сахара, которого в Haskell в большом достатке. Или list comprehensions тоже не используются? При этом в Haskell они есть, но через типы их сделать нельзя.
>Предметные области могут быть параметризованы. Предметная область "язык программирования" может быть параметризована предметной областью компилятор и предметной областью интерпретатор.
Это не параметризация предментной области, а просто различные реализации. Это было бы очень неприятно, если бы в случае с компилятором у нас был бы один язык, а с интерпретатором - другой. Не смотря на то, что в Haskell сделано именно так: ghci забрасывать пользователя прямиков внутрь IO-монады, после чего пользователь наслаждается всеми прелестями do-нотации, полезность которой вы отрицаете, я считаю это плохой практикой. При этом проблемы с реализаций различного поведения на макросах быть не должно. В одном случае сайд-эффекты будут применятся по ходу процесса разбора и типизации, а в другом - после.
>А можно использовать только систему типов. См. зависимые типы данных.
Simon Peyton-Jones и компания решили, что для монад и списков нужен сахар, как и во многих других местах. Если бы системы типов было бы достаточно, сахар бы не множился. Сама реальность языка Haskell опровергает это вашей утверждение. В этом констексте, интересно было бы узнать, что именно натолкнуло разработчиков Скалы на запиливание макросов в язык.
(no subject)
Date: 2012-07-16 05:44 pm (UTC)Я не про Хаскель говорил.
(no subject)
Date: 2012-07-16 05:51 pm (UTC)Эти понятия не применимы к макросам. Ты просо рассуждаешь о том что не понимаешь.
Макросы это языковые расширения и/или метапрограммы (в зависимости от типа и решаемой задачи).
Их не надо куда-то предавать. Их параметры это произвольное AST (код без интепретации). Так что в них можно передать что угодно но в виде кода. Их передавать никуда нельзя просто потому что это бессмысленно. Код же макросов - это произвольный код на Немерле. Его можно передавать куда угодно как и любую функцию и в нем можно использовать что угодно.
(no subject)
Date: 2012-07-16 06:30 pm (UTC)Vlad, иди ты нах-й!
(no subject)
Date: 2012-07-16 07:18 pm (UTC)Ты бы не лез рассуждать о том в чем некомпетентен. Тогда и в подобные ситуации не попадал бы. ;)
А если уж лезешь, то для начала изучи вопрос обсуждения. Противно ведь слушать подобную чушь.
(no subject)
Date: 2012-07-16 08:08 pm (UTC)(no subject)
Date: 2012-07-16 09:41 pm (UTC)(no subject)
Date: 2012-07-16 11:04 pm (UTC)Запомни этот опыт. Он тебе не раз ещё пригодится.
(no subject)
Date: 2012-07-16 05:58 pm (UTC)> Мы только что выяснили слабое место макросов. Макросы не являются первоклассной сущностью.
Мы только что выяснили отсутствие у тебя компетенции по обсуждаемому вопросу. Не более того.
Макросы - это точка входа в метамир (в компилятор). А внутри это просто код к который можно обернуть функцией (лябдой, например) и передавать куда угодно. Нужно только понимать, что это метакода (работающий на стадии компиляции), так что передать его в какие-то части компилируемой программы физически невозможно.
(no subject)
Date: 2012-07-16 06:01 pm (UTC)В Лиспе маросы очень похожие. Они так же работают во время "компиляции" программы. Только динамическая природа Лиспа позволяет интерпретировать код в ратайме. В остальном отличий нет. Макрос точно так же не первоклассная сущность в твоем понимании. Это метапрограмма занимающаяся переписыванием одного AST в другое.