вторник, 27 сентября 2011 г.

Как создавать DSL

Необходимость создания предметно-ориентированных языков (domain-specific languages, DSL) может быть продиктована разными причинами. Например, появлением новой технологической ниши, как было с HTML и SQL. Или крайней неуклюжестью давно занявших свои ниши языков общего назначения... не будем их называть. Ещё бывают причины чисто исторические. Неважно. Пусть вам пришла в голову идея сделать DSL. Сочинили вы язык, описали грамматику, продумали семантику, а дальше что? Как получить транслятор, отладчик, где взять удобную среду для написания кода на этом DSL? Не самому же всё делать с нуля. Вот об этом и речь.

Прежде всего надо определиться: DSL или EDSL? Второй вариант, который расшифровывается как «встроенный предметно-ориентированный язык» (embedded DSL, или internal DSL), сделает вашу жизнь значительно легче. Вы выбираете язык общего назначения, для которого все средства разработки уже сделаны до вас, и создаёте свой язык как надстройку над первым, оставаясь в рамках его грамматики. Фактически, вы делаете библиотеку. Само собой, если изначальный язык (хост-язык) недостаточно гибок в плане грамматических конструкций, то и от DSL особенного удобства ждать не приходится. Тем не менее, можно на Ruby, C#, Scala создавать вполне удобные EDSL. Даже на C++ с шаблонами можно создавать совершенно потрясающие вещи.

Вообще говоря, если вы выбрали EDSL — т.е. готовы жертвовать грамматикой языка ради готовых средств разработки — то почему бы не жертвовать до конца. Откажитесь от грамматики и задавайте программу сразу как экземпляр метамодели (что принято называть абстрактным синтаксическим деревом, хотя оно не абстрактное, да и к синтаксису отношения не имеет). Возьмите платформу, которая заточена под работу с такими программами. Речь, конечно, о семействе лиспов (Common Lisp, Scheme, Clojure). Никакого синтаксиса, кроме простейших S-выражений. Динамическая типизация. Система компилируемых макросов. Культура инкрементальной разработки. Лиспам нет равных в быстроте и удобстве создания EDSL. В знаменитой книге SICP мини-языки в рамках Scheme создаются практически под каждую задачу, и даже без использования макросов.

Мы подошли к месту, когда нельзя не упомянуть JetBrains MPS. Это весьма оригинальная вещь. В компании, где пишут среды разработки для мейнстримных языков программирования, зародилась идея создания IDE для создания IDE для создания DSL. По качеству аналогичных мейнстримным. С автодополнениями, рефакторингами, удобными отладчиками. Сами посудите, какой тут может быть лисп. В лиспе отсутствует культура автодополнений и рефакторинга, не развит статический анализ кода, зато повсюду кошмарные скобки. Лисп не годился. Пара вещей, однако, была заимствована из него: отказ от синтаксиса и макросы. Первое было даже не просто заимствовано, а доведено до абсолюта. В MPS нет ни парсера S-выражений, ни какого-либо другого парсера. Потому что в MPS нет кода. Редактор MPS работает не с кодом, а непосредственно с экземпляром метамодели. В целях облегчения перехода разработчиков на данную технологию редактор напоминает текстовый, декорируя и отображая дерево так, что оно выглядит как код, но им не является. Инструментов для импорта кода MPS не предоставляет (за исключением кода на Java). Что касается макросов для кодогенерации в MPS, то они там завязаны на строгую систему типов, что позволяет автоматически делать статическую проверку типов для создаваемого DSL.

Те, кто говорят, что MPS это переизобретение Лиспа, неправы. Скорее, MPS — это скачок к визуальному метапрограммированию будущего. Что не означает, однако, что этот скачок удачный. Время покажет. Возможно, молодое поколение будет очаровано визуальной средой MPS и автодополнениями. С другой стороны, им может помешать изолированность MPS от мира текстовых программ, т.к. сейчас практически все языки текстовые. Их также может отпугнуть тотальная бюрократизация всего процесса программирования (что неудивительно при хост-платформе Java). В общем, выбор делать им, а мы идём дальше, рассматривая тот случай, когда вам нужен именно текстовый DSL, со своей уникальной грамматикой.

Итак, вы желаете реализовать настоящий DSL. С транслятором проблем не будет — связка lex+yacc, портированная, кажется, на все платформы в мире, уже лет 40 выполняет задачу автоматического построения парсеров. Для грамматик, которым не хватает yacc, есть другие инструменты. Но отладчик и IDE придётся писать самостоятельно. Если только не... найти хост-язык с настраиваемой грамматикой и взять существующую IDE для него. Если она не сломается от введения новой грамматики. Фактически, получится EDSL, но без ограничений на синтаксис, которые накладывались бы нерасширяемым хост-языком. Этот подход называется «extensible programming»; он был довольно популярен в 1960-х, потом заглох, и ожил только в XXI веке. Поэтому живых языков с настраиваемой грамматикой не очень много.
  • Forth — старейший из таких языков. Отличается тем, что «своей» грамматики практически не имеет. Таков же его преемник Factor. Программирование на Forth, однако, может оказаться не таким простым.
  • Common Lisp отметился и здесь. Помимо defmacro, которые используются для создания «скобочных» EDSL, о которых было сказано выше, в CL есть т.н. reader macros, которые позволяют изменить поведение reader-а, т.е. расширить синтаксис. Чтобы не конфликтовать со стандартным синтаксисом CL, reader macros обычно начинаются с «#», хотя могут начинаться и с любого другого символа. Например, #c(0, 1) — запись мнимой единицы одним из стандартных макросов CL. Есть примеры более развесистых макросов: запуск shell-команд прямо из CL, вызов C-функций, синтаксис для хеш-таблиц, расширенный синтаксис строк, и даже встраивание XML непосредственно в лисповый код. Однако, если, синтаксис вашего DSL конфликтует с синтаксисом S-выражений, то дело может оказаться труднее. В теории, ничто не мешает изменить стандартный reader на собственный, но как это будет на практике, и как на это отреагирует ваша IDE (например SLIME), пока не ясно. Похоже, никто серьёзно не занимался этим вопросом в CL.
  • Racket — бывшая PLT Scheme — совсем другое дело. Платформа «из коробки» поддерживает концепцию переключения между разными языками, и позволяет создавать новые языки, используя при этом генератор парсеров в стиле yacc. Есть примеры «не-скобочных» языков, созданных на Racket, по мотивам Prolog, Brainfuck и Algol 60. Среда (DrRacket) и её отладчик работают с этими языками. Более того, DrRacket написан на Racket же, что открывает возможности для модификации среды под язык и распространения её в качестве IDE для вашего DSL. Словом, Racket — очень перспективная платформа для создания DSL, и поддерживается в прекрасном состоянии.
  • Nemerle — статически типизированный язык для .NET с макросами и расширяемым синтаксисом. Возможности расширения, однако, ограничены. Есть проект Nemerle 2, в котором ограничения будут сняты, и который обещает стать чем-то вроде MPS для .NET (но без отказа от грамматик). Пока только обещает, впрочем.
  • Bigloo — ещё одна реализация Scheme, с упором на быстродействие. Так же, как и Racket, обладает средствами генерации парсеров, и поддерживает макросы, однако не имеет такого удобного механизма переключения языков и создания новых. Кроме того, не факт, что отладчик Bigloo и его IDE (основанное на Emacs) будут нормально работать с новыми языками, ибо, как и с CL, вряд ли кто-то специально занимался этим вопросом, а само собой ничего не делается.
  • Helvetia — уникальная в своём роде разработка на базе языка Smalltalk, в которой используется то обстоятельство, что вся среда Smalltalk (включая парсер) поддаётся изменению в рантайме. Helvetia — инструмент для произвольного расширения синтаксиса Smalltalk. С сохранением отладки. И даже с подсветкой и автодополнением! Что делает MPS не совсем уникальной средой. Примеры включают SQL, Brainfuck и что-то похожее на CSS. Единственная неприятность — автор Helvetia защитил по ней PhD, ушёл работать в Google и забросил своё творение. Helvetia работает только на Pharo Smalltalk версии 1.1, и не портирована ни на современную версию Pharo (1.3), ни на Squeak. Однако автор иногда что-то делает, всё-таки. Жаль, что кроме него, никто в разработке Helvetia не участвует. Кстати, автор в 2009 г. выбрал в качестве хост-языка Smalltalk (а не Lisp) по причине «однородности» языка и среды. Среды разработки Smalltalk написаны на Smalltalk, а про Scheme или CL такого сказать было нельзя. DrRacket в то время был наполовину написан на C++, и его переписали на чистом Racket только в феврале 2011.

Как видите, в принципе есть инструменты на любой вкус и под любые требования. В наше время создавать DSL стало не только полезно, но и приятно.

Автор признателен LOR-у за подсказки и Ф.А.Новикову за вычитывание черновика.

1 комментарий:

Roman комментирует...

Как приятно читать статьи, написанные немейнстримно мыслящим человеком! Очень интересно и для общего развития полезно! А то, куда ни кинь, везде питон, руби, ява да дотнет!

С нетерпением жду продолжение! :)