Развитие идей метапрограммирования всегда было неким Граалем для индустрии программного обеспечения. Известно правило, что на сложность решаемых задач напрямую зависит от уровня абстракции, на котором строится его решение. Согласитесь, сложно было бы разрабатывать экспертные сестемы в терминах ассемблера. Поэтому-то и были разработаны технологии логического программирования, реализующие достаточную терминологическую базу для решения задач в области принятия решений. Вот техника метапрограммирования как дает в руки разработчиков неплохое средство повышения уровня абстракции при разработке систем путем расширения выразительных способностей компилятора.
Язык программирования Nermele во многом известен именно благодаря своей мощной системе метапрограммирования, которая достаточно органично вписывается в мир .NET, позволяя использовать язык со всеми его возможностями уже "здесь и сейчас". Язык предоставляет возможность разработки расширений к процессу компиляции с помощью максросов, написанных на этом же языке и подключаемых к нужному проекту как обычная библиотека.
Для того, чтобы опробовать все эти возможности, достаточно установить последний билд компилятора и интеграции с Visual Studio. Скачать это можно с сайта проекта интеграции. Установив Nemerle и интеграцию, в студии вам будет доступна новая группа проектов.
Пример, который мы рассмотрим будет достаточно простым и хорошо всем занкомым. Предположим, вы смоделировали предметную область и подошло время воплотить эту модель в коде. Каждая сущность этой модели будет представленная в виде неких персистентных классов. Кроме логики домена вам необходимо реализизовать т.н. логику приложения, которая включает в себя механизмы ORM, Concurrency и Audit Trail. Логика приложения привносит в каждый класс вашей модели свой специфичный набор полей:
- Каждый класс должен иметь поле идентификатора. Конечно, эта задача решается путем наследования от некого супер-класса. Но наследование может сыграть злую роль в задачах передачи данных за границы приложения. Например, в случаях какой-то экзотической сериализации. В случае распределенного приложения вообще лучше всего иметь классы с минимальной детализацией для передачи между подсистемами. Поэтому идеальным случаем было бы добавление поля идентификатора к каждому классу в домене;
- В задаче совместнго доступа нам нужно поле с временной меткой записи, по которой мы будем определять перед каждой операцией записи, была ли сущность изменена кем-то еще;
- Для простейшей системы аудита нам необходимо добавить поля, в которых будет хранится информация о том, кто запись создал и кто ее в последний раз изменял.
Итак, мы имеем 6 однотипных полей, которые мы должны добавлять в каждый создаваемый класс. Это и есть та самая рутина, которая не требует принятия каких-либо сложных решений и которую, благодаря этому, очень легко автоматизировать.
Пусть мы имеем следущий класс:
public class Enumeration { mutable value : string; mutable description : string; }
Его аналогом в C# был бы следующий код:
public class Enumeration { string value; string description; }
В этом классе не хватает публичных свойств к заданным полям. Это еще одна из рутинных операций, которые подлежат автоматизации. Для решения этой задачи мы воспользуемся готовым макросом, которые поставляется с компилятором:
public class Enumeration { [Accessor (flags = WantSetter)] mutable value : string; [Accessor (flags = WantSetter)] mutable description : string; }
Все просто: максросами-атрибутами вы указываете компилятору на то, что к данным полям необходимое сгенерировать свойства, причем, свойства должны быть с set-аксессором. Скомпилируем этот код и посмотрим рефлектором на то, как бы этот код выглядел на C#:
public class Enumeration { // Fields private string description; private string value; // Properties public string Description { [DebuggerStepThrough] get { return this.description; } [DebuggerStepThrough] set { this.description = value; } } public string Value { [DebuggerStepThrough] get { return this.value; } [DebuggerStepThrough] set { this.value = value; } } }
Но вернемся к нашим полям. С точки зрения разработчика было бы удобвно наделить класс Enumeration всем необходимым свойствами с помощью похожего макроса-атрибута:
[PersistentObject] public class Enumeration { ....
И выглядеть этот макрос будет следующим образом (для краткости мы будем добавлять только идентификатор):
using Nemerle; using Nemerle.Utility; [MacroUsage (MacroPhase.WithTypedMembers, MacroTargets.Class)] macro PersistentObject (t : TypeBuilder) { t.Define(<[ decl: [Accessor] mutable id : Guid; ]>); }
Как видите, ничем сложным разработка макросов не является. Изучим этот пример детальнее.
Атрибутом MacroUsage мы указываем компилятору на то, что макрос применяется в виде атрибута к классу и применяется на этапе, когда класс "наделен" всеми свойствами классов-предков. Так же мы можем "встроиться" в процесс компиляции до того, как класс будет унаследован.
Обязательным первым параметром макросов, которые применяются как атрибуты к классам является переменная типа TypeBuilder. Эта переменная указывает на объект, умеющий манипулировать целевым типом. Если бы наш макрос применялся к полям, то первый параметр был бы типа FieldBuilder.
Единственной операцией в нашем макросе является вызов методв Define у переменной типа TypeBuilder. Этот метод принимает в качестве параметра экземпляр синтаксического дерева, соответствующего коду, который мы хотим добавить в наш класс. Мы можем, конечно же создать это экземпляр вручную, добавляя все синтаксические элементы, как узлы этого дерева. Но Nemerle предоставляет нам возможность избежать этого, позволяя нам описать не код, строящий дерево, а код, который и должен быть представлен в виде дерева. Эта возможность называется "цитированием". Для цитирования используется подобная конструкция:
def ast : PExpr = <[ /* Код, который будет представлен в виде дерева */ ]>
Обратите внимание, что мы внутри макроса использовали другой макрс, а именно, известный нам Accessor. Таким образом класс Enumeration будет расширен приватным полем и свойствам к нему.
Забегая вперед, хочется отметить, что возможности макросистемы Nemerle позволяют добавлять новые классы в сборки, основанные на метаданных. Например, мы можем расширить наш макрос PersistentObject так, чтобы он для каждого класса генерировал все необходимые классы DAO и даже подготавливал базу данных. И все это на этапе компиляции!...