Про синтаксические макросы в Nemerle. Часть 2

Jul 16, 2012 11:53

Продолжение о синтаксических макросах немерле. Начало тут.


Макрооператоры

Я уже выше показывал, как можно с помощью макросов вводить новый синтаксис. Но существует еще один способ - макрооператоры. С их помощью можно определять новые операторы. Вариант с перегрузкой операторов как в C# никто не отменял, но в некоторых ситуациях хочется определить оператор для типов, к которым нет доступа, либо семантика оператора такова, что к какому-то конкретному типу его привязать сложно.

В качестве примера выкладываю код петуш оператора, который имитирует питоновское умножение числа на строку, то есть просто повторяет строку указанное число раз:

copy to clipboardподсветка кода
  1. [assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "*>", false, 259, 259)]    
  2. macro @*>(str : PExpr, mult : PExpr) {  
  3.     StringMultImpl.DoTransform(Macros.ImplicitCTX(), str, mult)  
  4. }  
  5.   
  6. module StringMultImpl {  
  7.   
  8.     public DoTransform(typer : Typer, str : PExpr, mul : PExpr) : PExpr {  
  9.         Macros.DefineCTX(typer);  
  10.         def strType = typer.TypeExpr(str).Type;  
  11.         when(!strType.TryRequire(typer.BindType(<[string]>)))   
  12.             Message.Error(str.Location, $"Operator *> left argument requared string, got $strType");  
  13.         def mulType = typer.TypeExpr(mul).Type;  
  14.         when(!mulType.TryRequire(typer.BindType(<[int]>)))   
  15.             Message.Error(str.Location, $"Operator *> rigth argument requared int, got $mulType");  
  16.         <[string.Join("", NList.Repeat($str, $mul));]>  
  17.     }  
  18. }  


Существенным в этом примере, является атрибут уровня сборки
[assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "*>", false, 160, 161)]
в котором описываются параметры оператора:
- пространство имен, в котором определен оператор,
- его имя (Кстати можно использовать не только ХАСКИАРТ, но любые другие символы. С таким же успехом оператор мог называться strReply)
- является ли оператор унарным (в противном случае - бинарный. КО)
- сила связывания слева
- сила связывания справа

Сила связывания задает ассоциативность оператора. Если сила слева меньше чем справа, оператор будет левоассоциативным и наоборот. Кроме того она задает приоритет оператора. Это работает неочевидным образом, по крайней мере для меня, потому я покажу это на примере для выражения "ООО"+"ЗАО" *> 3*3+4:

copy to clipboardподсветка кода
  1. Силы (300, 300) → "ООО" + string.Join("", NList.Repeat("ЗАО", 3)) * 3 + 4  
  2. Силы (260, 260) → "ООО" + string.Join("", NList.Repeat("ЗАО", 3 * 3)) + 4  
  3. Силы (239, 239) → string.Join("", NList.Repeat("ООО" + "ЗАО", 3 * 3 + 4))  
  4. Силы (239, 240) → string.Join("", NList.Repeat("ООО" + "ЗАО", 3 * 3)) + 4  
  5. 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подсветка кода
  1. [assembly: Nemerle.Internal.OperatorAttribute ("ArticleMacros", "?", false, 142, 139)]  
  2. macro @?(dynObj, expr) {  
  3.     DynamicAssignmenImpl.DoTransform(Macros.ImplicitCTX(), dynObj, expr)  
  4. }  
  5.   
  6. module DynamicAssignmenImpl{  
  7.     public DoTransform(typer : Typer, dynObj: PExpr, expr: PExpr) : PExpr {  
  8.         Macros.DefineCTX(typer);  
  9.         def dynObjType = typer.TypeExpr(dynObj);  
  10.         when(! dynObjType.Type.TryRequire(typer.BindType(<[ IDictionary[string, object] ]>))) {  
  11.             def msg = $"Required IDictionary[string, object], but got $(dynObjType.Type)";  
  12.             Message.Error(dynObj.Location, msg);  
  13.         }  
  14.         match(expr) {  
  15.             | <[$(prop: name) = $e]> =>   
  16.                     def key = prop.ToString();  
  17.                     <[ if($dynObj.ContainsKey($key))   
  18.                         $dynObj[$key] = $e;  
  19.                     else   
  20.                         $dynObj.Add($key, $e); ]>  
  21.             | <[$(prop: name)]>      =>   
  22.                     <[ $dynObj[$(prop.ToString())] ]>;  
  23.             | <[$e]>                 =>   
  24.                     def msg = $"Expected syntx dyn?PropertyName or dyn?PropertyName = value, got $e";  
  25.                     Message.Error(expr.Location, msg);  
  26.                     <[()]>     
  27.         }  
  28.     }  
  29. }  


Мой оператор умеет не только добавлять пару ключ-значение в словарь, но и обновлять значение, если ключ уже существует, а также возвращать значение по ключу, если пользователь ничего присваивать не стал. При этом работа с таким оператором, мало отличается от работы с родным dynamic из C#

copy to clipboardподсветка кода
  1. //Небольшое, но гордое русскоязычное комьюнити немерлистов  
  2.        //испытывает сильнейшую боль от ентой точки  
  3.        def dict = Dictionary.[string, object]();  
  4.        dict?LOL = 9+8;  
  5.        dict?YOBA = DateTime.Now;  
  6.        WriteLine($"LOL = $(dict?LOL); YOBA = $(dict?YOBA)");  
  7.        dict?YOBA = "Another YOBA";  
  8.        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. Для меня сейчас они представляют наибольший интерес, поскольку именно через них осуществляется столь необходимая мне кодогенерация. Но об этом как-нибудь в другой раз.

мысли, nemerle, fp, программирование

Previous post Next post
Up