Статьи

Шаблоны во внутренних реализациях DSL

Недавно я подумал, что классификация DSL как внутренних и внешних слишком широка, учитывая множество архитектурных шаблонов, с которыми мы сталкиваемся в различных реализациях. Я предполагаю, что более интересные реализации находятся во внутреннем жанре DSL, начиная с простых старых свободно распространяемых интерфейсов, которые в основном популяризировал Мартин Фаулер, и заканчивая очень сложным полиморфным внедрением, которое недавно было продемонстрировано в Scala.

Мне нравится использовать термин « встроенный», а не «внутренний», поскольку он явно указывает на то, что DSL поддерживает инфраструктуру существующего языка (он же является основным языком DSL). Это часть общности всех встроенных DSL. Но DSL — это не что иное, как хорошо продуманные абстракции, достаточно выразительные для конкретной области использования. Помимо этой общности, внутренние реализации DSL также демонстрируют систематические различия в форме, функциях и архитектуре. Цель этого поста — выявить некоторые явные и интересные шаблоны, которые мы находим среди сегодняшних встроенных реализаций DSL.

Простые старые Smart API, плавные интерфейсы

Достаточно было задокументировано этой доминирующей идиомы, в основном используемой в сообществе Java и C #. Вот один из моих недавних фаворитов ..

ConcurrentMap<Key, Graph> graphs = new MapMaker()
.concurrencyLevel(32)
.softKeys()
.weakValues()
.expiration(30, TimeUnit.MINUTES)
.makeComputingMap(
new Function<Key, Graph>() {
public Graph apply(Key key) {
return createExpensiveGraph(key);
}
});

 

Мой хороший друг Серхио Босса недавно внедрил симпатичный DSL на основе умных сборщиков для обмена сообщениями в Actorom.

on(topology).send(EXPECTED_MESSAGE)
.withTimeout(1, TimeUnit.SECONDS)
.to(address);

Actorom — это полная реализация актера на основе Java. Выглядит очень многообещающе — иди проверь это ..

Тщательно реализованные плавные интерфейсы, использующие шаблон компоновщика, могут быть семантически надежными и сохранять порядок. Вы не можете вызывать элементы цепочки не по порядку и придумывать несогласованную конструкцию для вашего объекта.

Генерация кода с использованием метапрограммирования во время выполнения

Мы наблюдаем огромный всплеск интереса к метапрограммированию во время выполнения с ростом популярности таких языков, как Groovy и Ruby. Оба эти языка реализуют протоколы мета-объектов, которые позволяют разработчикам манипулировать мета-объектами во время выполнения с помощью методов синтеза методов, перехвата методов и обработки времени выполнения строк кода.

Code generation using compile time meta-programming

I am not going to talk about C pre-processor macros here. They are considered abominations compared to what Common Lisp macros have been offering since the 1960s. C++ offers techniques like Expression Templates that have been used successfully to generate code during compilation phase. Libraries like Blitz++ have been developed using these techniques through creation of parse trees of array expressions that are used to generate customized kernels for numerical computations.

But Lisp is the real granddaddy of compile time meta-programming. Uniform representation of code and data, expressions yielding values, syntactic macros with quasiquoting have made extension of Lisp language possible through user defined meta objects. Unlike C, C++ and Java, what Lisp does is to make the parser of the language available to the macros. So when you write macros in Common Lisp or Clojure, you have the full power of the extensible language at your disposal. And since Lisp programs are nothing but list structures, the parser is also simple enough.

The bottom line is that you can have a small surface syntax for your DSL and rely on the language infrastructure for generating the appropriate code during the pre-compilation phase. That way the runtime does not contain any of the meta-objects to be manipulated, which gives you an edge over performance compared to the Ruby / Groovy option.

Explicit AST manipulation using the Interpreter Pattern

This is yet another option that we find being used for DSL implementation. The design follows the Interpreter pattern of GOF and uses the host language infrastructure for creating and manipulating the abstract syntax tree (AST). Groovy and Ruby have now developed this infrastructure and support code generation through AST manipulation. Come to think of it, this is really the Greenspunning of Lisp, where you can program in the AST itself and use the host language parser to manipulate it. While in other languages, the AST is far away from the CST (concrete syntax tree) and you need the heavy-lifting of scanners and parsers to get the AST out of the CST.

Purely Embedded typed DSLs

Unlike pre-processor based code generation, pure embedding of DSLs are implemented in the form of libraries. Paul Hudak demonstrated this with Haskell way back in 1998, when he used the techniques of monadic interpreters, partial evaluation and staged programming to implement purely embedded DSLs that can be evolved incrementally over time. Of course when we talk about typed abstractions, the flexibility depends on how advanced type system you have. Haskell has one and offers functional abstractions based on its type system as the basis of implementation. Amongst today’s languages, Scala offers an advanced type system and unlike Haskell has the goodness of a solid OO implementation to go along with its functional power. This has helped implementing Polymorphically Embeddable DSLs, a significant improvement over the capabilities that Hudak demonstrated with Haskell. Using features like Scala traits, virtual types, higher order generics and family polymorphism, it is possible to have multiple implementations of a DSL on top of a single surface syntax. This looks very promising and can open up ideas for implementing domain specific optimizations and interesting variations to coexist on the same syntax of the DSL.

Are there any interesting patterns of internal DSL implementations that are being used today?

From http://debasishg.blogspot.com