Языково-ориентированное программирование: следующая парадигма

Автор: Сергей Дмитриев
JetBrains

Перевод: Зверёк Харьковский
Источник: http://jetbrains.com/mps
Материал предоставил: RSDN Magazine #5-2005
Опубликовано: 02.03.2006
Версия текста: 1.0
Введение
Часть I. Обзор Языково-ориентированного программирования
Языково-ориентированное программирование и Meta Programming System
Что не так с мейнстримовым программированием
Детали ЯОП
Часть II. Введение в Meta Programming System
Создание языков в MPS
Совместное использование языков
Платформы, фреймворки, библиотеки и языки
Начало работы с MPS
Выводы
Благодарности
Ссылки
Статьи:
Книги:
Другие Web-ресурсы:

Введение

Пришло время следующей технологической революции в разработке ПО– и эта революция приобретает все более ясные очертания. Новая парадигма программирования уже почти перед нами. Она еще не вполне сформирована – разные части известны под разными именами вроде Intentional Programming, MDA, порождающее программирование и т.д. Я предлагаю объединить эти новаторские подходы под общим именем «языково-ориентированного программирования»; данная статья объясняет основные принципы этой новой парадигмы.

Сегодняшний основной подход к программированию основывается на нескольких ключевых допущениях, которые держат нас на привязи, хотя многие программисты этого не осознают. Несмотря на стремительный прогресс, мы все еще в каменном веке программирования. Наш старый надежный каменный топор (объектно-ориентированное программирование) хорошо послужил нам, но трескается и рассыпается на сегодняшних проблемах. Чтобы развиваться дальше, пора приручить огонь. Только так мы сможем выковать новые инструменты и войти в век изобретений и новых технологий.

Я говорю о тех ограничениях, которые заставляют программиста думать как компьютер, вместо того чтобы научить компьютер думать по-человечески. Это серьезные, закоренелые ограничения, и преодолеть их будет непросто. Говоря об абсолютно новой парадигме, я не преувеличиваю. Нам придется полностью изменить способ написания программ.

Эта статья описывает мой взгляд на языково-ориентированное программирование (ЯОП) и результаты моей работы в этом направлении. В первую очередь, я опишу проблемы сегодняшнего подхода к программированию, затем объясню концепции ЯОП с примерами моей реализации парадигмы, Meta Programming System (MPS). Статья должна показать перспективу ЯОП с высоты птичьего полета, вызвать интерес к самой идее и стать базой для дальнейших обсуждений и отзывов.

Часть I. Обзор Языково-ориентированного программирования

Языково-ориентированное программирование и Meta Programming System

В идеале, профессия программиста дает мне возможность делать с компьютером все. В реальности, свобода сегодняшнего программиста весьма ограниченна. Да, конечно, я могу сделать все, но многое из этого «всего» займет несколько лет – а должно бы существенно меньше. Что-то здесь неправильно.

Программиста ограничивает зависимость от инфраструктуры, которую нельзя изменить – в частности, языков программирования и сред разработки. Если мне нужно некоторое расширение языка, придется подождать, пока его внесет автор. Если мне нужны дополнительные функции в IDE, придется ждать, пока это сделает производитель. Эти-то зависимости и ограничивают мою свободу. Конечно, можно написать собственный компилятор или IDE. Например, именно поэтому я начинал работу над IntelliJ IDEA – надоело зависеть от недостатков существующих IDE для Java. Но это отнимает огромное количество времени и сил и совершенно непрактично. Вот где кроется разница между теоретической и практической свободой. Ниже, когда я говорю о свободе, я имею в виду практическую.

Чтобы увеличить степень свободы, нужно уменьшить степень зависимости. Например, одна из главных целей Java – уменьшить зависимость от операционной системы и дать разработчикам свободу распространения программ для любых ОС. Так что для освобождения от языков и сред придется уменьшить свою зависимость от них.

В чем же проблема? Любой язык общего назначения, будь то Java или C++, дает возможность сделать с компьютером что угодно. Это так, по крайней мере, в теории, но, как показано ниже, языки общего назначения недостаточно продуктивны. Как альтернатива, существуют языки, специфичные для предметной области (DSL, они же “малые языки”), чья цель – максимальная продуктивность в своей области – как, например, SQL для баз данных. Сила этих языков – специфичность для предметной области – в то же время и их слабость, ведь реальные программы работают со множеством предметных областей.

Дело не в противостоянии языков общего направления и специфичных. Мне нужна свобода во всем. Мне нужно делать что угодно с максимальной продуктивностью. На данный момент, способов достичь этой цели не существует. В идеале, мне нужна возможность использовать разные языки для разных частей программы и эффективно совмещать их. Среда должна поддерживать все эти языки – обеспечивать рефакторинг, авто-дополнение, навигацию и прочие инструменты продуктивности, доступные для мейнстримовых языков.

Чтобы достичь этой независимости, мне нужна свобода создавать, использовать и модифицировать языки и среды разработки. Чтобы свобода была практичной, все это должно достигаться легко. Решение проблемы легкости создания языков и сред станет огромным скачком вперед для программистов. Вот для чего нужно языково-ориентированное программирование.

Чтобы понять, что же такое ЯОП, давайте взглянем на сегодняшнее мейнстримовое программирование. Оно работает примерно так:

Обдумать: есть задача, которую надо запрограммировать; вы формируете в голове концептуальную модель решения.

Выбрать: вы выбираете некий язык программирования общего назначения (например, Java или C++), на котором будет написано решение.

Запрограммировать: пишете решение, выполняя сложнейшее отображение своей концептуальной модели на выбранный язык.

Шаг «запрограммировать» – узкое место, поскольку отображение выполняется тяжело, и в большинстве случаев не естественным образом (рис.1). Этот метод показал свою неэффективность в создании сложных программ. Для сравнения, вот как работает ЯОП:

Обдумать: есть задача, которую надо запрограммировать; вы формируете в голове концептуальную модель решения.

Выбрать: вы выбираете некие специальные DSL, на которых будет написано решение.

Создать: если нужных DSL еще не существует, вы их создаете.

Запрограммировать: пишете решение, являющееся относительно прямым отображением концептуальной модели на DSL.


Рисунок 1. Программирование на языке общего назначения.

В этом случае шаг «запрограммировать» в существенно меньшей степени является «узким местом», так как DSL облегчают превращение концепции в нечто, понятное компьютеру (рис. 2). Может показаться, что сложность просто сдвинута на шаг «создать». Однако совмещение подходящих инструментов и самоприменимости ЯОП существенно облегчает этот шаг.


Рисунок 2. Языково-оринетированное программирование на языке предметной области.

Мотивация ЯОП приблизительно такова: мне нужна возможность работать в терминах концепций и понятий проблемы, которую я решаю, вместо того чтобы переводить свои мысли в нотацию языка программирования общего назначения (классы, методы, циклы, ветвления и т.п.). Для этого мне нужен язык, специфичный для предметной области. Где его взять? Создать.

Я начал разработку универсальной платформы разработки предметно-специфичных языков (Meta Programming System), а так же инструментов и средств для них. Она позволит программисту определять языки с той же легкостью, с которой сегодня пишутся программы. Платформа полностью поддерживает ЯОП, давая свободу использования наиболее удобного языка для каждой части программы взамен привязанности к единственном языку общего назначения.

MPS – лишь пример использования языково-ориентированного программирования. Я использую в качестве примера MPS, но ЯОП можно реализовать различными средствами. Концепция ЯОП – это не то же, самое что его реализация, так же, как концепция ООП – это не Java, C++ или Smalltalk.

Что не так с мейнстримовым программированием

Как говорит старая пословица, «работает? – не трогай!» Сегодняшнее программирование не работает. Я вижу в нем множество проблем, и большинство их проистекает из того, что нет способа обеспечить хорошую поддержку отдельной предметной области языком общего назначения, как нет и универсально предметно-специфичного языка. Ниже описаны три наибольших проблемы современного программирования, решаемые в ЯОП.

Временные задержки в реализации идей

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

К примеру, сегодня значительная часть времени разработки тратится на объектно-ориентированное проектирование. В целом, это весьма творческий процесс составления классов, иерархий, отношений и т.п. Цель этого занятия – выразить программу в объектно-ориентированных терминах классов и методов. Процесс проектирования необходим, потому что классы и методы – единственные абстракции, понятные объектно-ориентированным языкам. Казалось бы, процесс необходимый и творческий, но в рамках ЯОП объектно-ориентированное проектирование не нужно вообще.

Анализ и поддержка существующего кода

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

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

Кривая обучения для предметной области

Третья основная проблема – в предметно-ориентированных расширениях языка. К примеру, в ООП основной метод расширения языка – библиотеки классов. Проблема в том, что библиотеки выражаются не в терминах предметной области, а в низкоуровневых обобщенных абстракциях вроде классов и методов. Библиотеки редко напрямую выражают предметную область. Они вводят дополнительные сложности (вроде поведения классов) для полноты отображения. Пара хороших и общих примеров – библиотеки графического интерфейса и баз данных.

Изучение таких библиотек – задача непростая даже для эксперта в данной области. Поскольку прямого отображения предметной области в язык не существует, приходится изучать отображение, что дает крутую кривую обучения. Изучение документации и примеров помогает, но занимает много времени. Усложняясь, библиотека становится намного более сложной для изучения, и программисты теряют желание с ней разбираться.

И даже для изучившего это сложное отображение остается вечная опасность ошибочного использования библиотеки: среда (компилятор и редактор) не помогает использовать ее правильно. Для этих инструментов вызов метода объекта GUI ничем не отличается от вызова метода объекта БД – и то и другое всего лишь вызов метода, ничего более. Какие методы каких классов вызывать, в каком порядке и т.п. – остается заботой пользователя.

Даже будучи экспертом и в предметной области и в использовании библиотеки, вы не избежите проблемы многословности программы. Относительно простые предметные концепции требуют сложного набора телодвижений. Об этом знает любой, использовавший, например, Swing (библиотека классов для реализации GUI в Java). Слишком много времени требуется для записи простых вещей – и еще больше для записи сложных.

Детали ЯОП

Что такое программирование в ЯОП?

Сегодня 99% программистов считают, что программирование есть запись инструкций, которым должен следовать компьютер. Нас учили, что компьютеры смоделированы по модели машины Тьюринга, так что они «думают» в терминах наборов инструкций. Но такой взгляд на программирование неполноценен: он путает способы и цели программирования. Я покажу, чем ЯОП лучше традиционного программирования, но в первую очередь необходимо кое-что прояснить: программа в ЯОП – это не набор инструкций. Тогда что это?

Имея проблему, я обдумываю ее решение. Это решение выражено в словах, понятиях, концепциях, мыслях – как бы вы их не называли. Это модель решения в моей голове. Практически никогда я не думаю о наборах инструкций, вместо этого я думаю о взаимосвязанных концепциях, специфичных для области в которой я работаю. К примеру, рассуждая о GUI, я думаю «Эта кнопка должна быть там, это поле тут, а в комбобокс надо поместить набор значений». Я могу даже просто представлять картинку, вообще не используя слов.

Я называю эту мысленную модель решением: я могу объяснить ее другому программисту с достаточной степенью детализации, чтобы он мог сесть и написать программу (скажем, на Java), решающую проблему. Мне не нужно выражать решение в терминах языка программирования – оно может быть в любой форме. Объясняя GUI, я рисую картинку. Это предметно-ориетированное представление должно быть программой. Другими словами, должен быть способ использовать это представление как готовую программу, а не только как способ общения с другими программистами. Отсюда следует мое неформальное определение программы: программа есть любое однозначно описанное решение проблемы. Или, более точно: программа есть любая точно определенная модель решения некоторой проблемы в некоторой предметной области, выраженная через понятия предметной области.

Поэтому я считаю, что программисты должны иметь свободу создания собственных языков – чтобы выражать решения в более естественном виде. Языки общего назначения точны, но слишком многословны и подвержены ошибкам. Естественные языки (скажем, английский) богаты, но слишком сложны из-за неформальности и неоднозначности. Нужна возможность легко создавать формальные, точно определенные, предметно-ориентированные языки. Языково-ориентированное программирование – это не просто написание программ, это создание языков для их написания. Программы должны быть ближе к предметной области, чем к компьютерным наборам инструкций, и писать их должно быть намного проще.

Программы и текст

Все привыкли, что программа хранится в виде текста – потока символов. Почему бы и нет? В конце концов, существует огромное количество инструментов для редактирования, отображения и манипуляции текстом. Главная часть сегодняшних языков программирования – грамматики, парсеры, компиляторы, построчные отладчики. Однако, текст – лишь одно из возможных представлений программы. Программы – это не текст. Привычка к текстовой форме программ вызывает огромное количество неожиданных трудностей. Нужен другой способ хранения программ и работы с ними.

При компиляции исходного кода компилятор представляет текст в виде древовидного графа, называемого «абстрактным синтаксическим деревом». Ту же самую операцию программист выполняет в уме, читая исходники. Мы продолжаем представлять программу как дерево. Именно отсюда все эти круглые-квадратные-фигурные скобки. Именно поэтому нужно форматировать код, делать отступы, следовать соглашениям о кодировании – чтобы легче читать исходники.

Почему же мы храним верность тексту? Потому что на сегодняшний день наиболее удобный и универсальный способ читать и писать программы – с помощью текстового редактора. Но за это приходится платить множеством неудобств, и главное из них – текстовые языки программирования очень тяжело расширять. Если программа хранится в виде текста, для ее разбора нужна однозначная грамматика. Чем больше возможностей добавляется в язык, тем труднее добавлять новые расширения, не создавая неоднозначностей. Нужно больше типов скобок, операторов, ключевых слов, правил порядка и вложенности и проч. Разработчики языков проводят неимоверное количество времени в размышлениях о синтаксисе и попытках расширить его.

Если мы хотим сделать создание языков легким, нужно отделить представление и хранение программы от самой программы. Мы должны хранить программы прямо в виде структурированного графа, что дает возможность вносить любые дополнения в язык. Временами текстовое хранилище вообще не нужно. Хорошим примером может послужить Excel. 99% пользователей совершенно все равно, в каком формате хранятся данные – в случае чего, всегда есть импорт и экспорт. Единственная причина использования текста – отсутствия редактора лучшего, чем текстовый. Но это поправимо.

Проблема в том, что текстовый редактор глуп и не умеет работать с графом, лежащим в основе программы. Но при помощи соответствующих инструментов редактор сможет напрямую взаимодействовать с графом программы, что даст свободу выбора визуального представления. Можно представить программу текстом, таблицами, диаграммами, деревьями – чем угодно. Можно и вовсе использовать разные представления для разных целей – скажем, графическое для просмотра и текстовое для редактирования. Можно использовать предметно-ориентированное представление различных частей кода, например, специальных математических символов в формулах, графиков, колонок и столбцов в таблицах, и т.п. Для каждой предметной области можно использовать наиболее подходящее представление – в том числе и текст, но не только текст. Какое представление будет лучшим – зависит от того, как мы думаем о предметной области. Такая гибкость представления даст возможность создания наиболее мощных редакторов: разные представления требуют разных способов редактирования.

Что такое «язык» в терминах ЯОП?

В конце концов, нужно уточнить, что имеется в виду под термином «язык». В ЯОП язык определяется тремя главными факторами: структурой, редактором, семантикой. Структура определяет абстрактный синтаксис языка, поддерживаемые концепции и способ их объединения. Редактор определяет конкретный синтаксис, способы его отображения и редактирования. Семантика определяет поведение, способ интерпретации и/или преобразования в исполняемый код. Естественно, язык может иметь и другие аспекты, к примеру, ограничения и систему типов.

Часть II. Введение в Meta Programming System

Создание языков в MPS

Я объяснил, зачем необходима возможность легко создавать новые языки. Но как сделать их создание легким? Если посмотреть на это с другой стороны и применить языково-ориентированное программирование к нему самому, легко увидеть ответ. Получившаяся тавтология может озадачить, но будьте спокойны. Единственный раз разобравшись, вы вполне осознаете мощь ЯОП.

Вспомним, что основная цель ЯОП – упростить создание предметно-ориентированных языков, а языки эти должны упростить создание программ. Но, как показано выше, понятие «программы» в ЯОП не ограничивается привычной «программой-как-набор-инструкций». Любое однозначное решение некоторой проблемы в некоторой области – это «программа». Так что если выбрать область «создание новых языков», то «программой» в этой области будет как раз определение нового языка – решение не хуже любого другого.

Таким образом, применяя идеи ЯОП, для упрощения «создания новых языков» нужно создать специальные DSL для области «создание новых языков». А уже использование этих «языко-строительных» DSL упрощает создание новых языков. Рассмотрим несколько языко-строительных языков, чтобы показать, как это работает. Это только краткий обзор – следующие статьи определят эти языки более детально.

Язык структуры

Как необходимый минимум, необходимо определить «структуру» нового языка. Это позволит писать «точно определенные» программы. Структура языка вовсе не означает его текстовую грамматику – как и было сказано, язык может вообще не иметь текстового представление, а только графовое.

В большинстве случаев, используя ЯОП, вы работаете на двух «уровнях» программирования – на мета-уровне и на программном уровне. Определив язык на мета-уровне, вы пишете программу на программном уровне. Определяя структуру нового языка, вы используете DSL языковой структуры для определения нового языка, который будет использован на программном уровне.

В MPS каждый узел на программном уровне имеет «тип», который является просто связью с другим узлом мета-уровня. Узел программного уровня – это как бы «экземпляр» типа. Узел «тип» мета-уровня определяет свойства экземпляра и отношения, допустимые для него. Язык описания языка мета-уровня называется просто Языком Структуры.

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


Рисунок 3. Определение концепции «метод» на языке структуры.

Возможны два типа отношений. Первый – отношение агрегации, формирующее древовидную структуру «родитель-потомок» для моделей концепций. Второй тип отношений – неагрегирующие «свободные» отношения, которые могут связывать любой узел с любым другим. У отношения есть два конца – «источник» и «цель». Отношения имеют роли, для каждой из которых определено имя, мощность каждого конца и тип целевого узла. Возможные значения мощности – 1, 0..1, 0..n, 1..n, что позволяет ограничить количество связей в рамках данного отношения. Тип целевого узла используется для ограничения типов узлов, которые могут быть связанными.

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

Язык редактора

Каким будет интерфейс для создания моделей и манипулирования ими? Для новых языков нужен редактор. Но обобщенный редактор нас не устроит; опыт показывает, что обобщенный редактор недостаточно удобен. Мы хотим писать модели быстро, а значит – нужны специализированные редакторы, скроенные в соответствии с концепцией языка. Некоторым образом редактор является частью языка, и коль скоро наша цель – легкость создания языков, то она включает в себя и легкость создания редакторов. По сути, нужен язык создания редакторов. В MPS он называется язык редактора.

Я уверен, что большинство людей, услышав о хранении программ как графов и специализированных редакторов, считают, что я говорю о редакторе схем. Это не так. Несмотря на то что программа есть граф, редактор не обязан отображать ее как схему. Фактически, редактирование схем может быть полезно в очень небольшом проценте случаев (то есть там, где это уместно – скажем, для таблиц БД). При создании языка редактора у нас был совсем другой источник вдохновения – как ни смешно, но это текстовые редакторы.

Среднестатистическую программу в текстовом редакторе можно представить разделенной на прямоугольные ячейки. В некоторых ячейках обязательные символы: ключевые слова, скобки; в других – пользовательские символы: имена классов и методов. Большие ячейки состоят из меньших – так же, как блок метода содержит выражения, в которых могут быть собственные вложенные блоки. Фактически, любая корректная программа в традиционном программировании может быть представлена набором ячеек. Ну а в языке редактора не нужно воображать эти ячейки – редактор в самом деле состоит из них (рисунок 4).


Рисунок 4. Определение редактора для концепции «Метод»

Использование ячеек дает некоторые интересные преимущества. Во-первых, ячейки могут прекрасно «притвориться» текстовым редактором, работая при этом напрямую с графом, а не текстом. Во-вторых, ячейки не ограничены текстом; в них может быть что угодно: контрол выбора цвета, математические символы, графики, векторные рисунки – что угодно. И, наконец, даже такое ячеечное расположение необязательно – программист может выбрать другой механизм. Ячейки – просто полезное умолчание.

Язык редактора позволяет определить расположение ячеек для каждой концепции языка. Можно определить неизменные части, скобки и прочие украшения, а также переменные части – и потребовать у пользователя определить их. Язык редактора позволяет добавить в редактор мощные возможности авто-дополнения, рефакторинга, навигации, подсветки синтаксиса и ошибок, и все остальное, что вы только можете вообразить. Таким образом, в вашем языке может быть задействована вся мощь современных редакторов класса IntelliJ IDEA. Это возможно благодаря графовой структуре программы и специализированному языку редактора, помогающему создавать мощные редакторы.

Язык преобразований

Язык структуры и язык редактора в сумме уже дают немалую мощь. Их можно использовать для обмена идеями – например, рисования UML-диаграмм и других типов статической документации. Тем не менее, хотелось бы, чтобы наш код делал нечто – для этого нужен способ сделать его исполняемым. Есть два пути добиться этого: интерпретация и компиляция.

Интерпретацию обеспечивают DSL, помогающие определить, как компьютер должен интерпретировать программу. Компиляцию – языки, помогающие определить, как генерировать из программы исполняемый код. Поддержка интерпретации будет описана в дальнейших статьях. Ниже я показываю, как MPS поддерживает компиляцию.

Компиляция есть генерация некоторой формы исполняемого кода по исходному. Существует много возможностей добиться результата. Чтобы сгенерировать исполнимый код, можно сгенерировать машинный код, или байткод для запуска на виртуальной машине. Как вариант, можно сгенерировать исходный код на другом языке (скажем, Java или C++), а затем использовать существующий компилятор, чтобы превратить его в исполнимый. В конце концов, можно сгенерировать исходник на интерпретируемом языке и выполнить его с помощью интерпретатора.

Чтобы избежать проблемы выбора, мы решили делать в MPS сразу все. Сначала вы определяете целевой язык в MPS, используя язык структуры. Этот целевой язык должен иметь прямое, «один-к-одному», отображение в целевой формат. К примеру, если целевой формат – машинный код, в MPS определяется целевой язык, представляющий машинный код; если целевой формат – исходники на Java, определяется Java-подобный целевой язык. Целевой язык не обязан поддерживать все возможности целевого формата, достаточно прямого отображения всех возможностей, которые вам нужны.

Таким образом, компиляция проходит в две фазы: простая трансляция целевого языка в финальный результат, и более сложная трансформация основного, исходного языка в промежуточный целевой. Фаза трансляции тривиальна, так что обратим внимание на более интересную фазу трансформации. По сути, проблема сведена к тому, чтобы перевести модель с одного языка на другой. Но исходный и целевой языки могут отличаться радикально, что усложняет трансформацию – например, если один узел исходного языка должен отобразиться на множество узлов целевого, разбросанных по целевой модели. Необходимо максимально облегчить определение трансформаций, и для этого требуется DSL трансформации. В MPS он называется язык трансформации.

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

Три подхода объединяются путем определения соответствующих DSL. DSL совместно участвуют в определении трансформаций с одного языка на другой. К примеру, итеративный подход привел к созданию языка запросов модели (Model Query Language), облегчающего перебор узлов и сбор информации о концептуальной модели. Это нечто вроде SQL для концептуальных моделей. Вдобавок, существование мощного языка запросов полезно не только при кодогенерации (а, например, и для того, чтобы сделать редактор более «умным»).

Шаблоны

Шаблонный подход в чем-то похож на Velocity и XSLT. Шаблоны выглядят как целевой язык, но позволяют добавлять макросы в любую часть шаблона. Макросы – это просто кусочки кода, исполняемые в момент трансформации. Макросы позволяют извлекать информацию из модели (используя язык запросов модели) и использовать эту информацию для «заполнения пропусков» в шаблоне при генерации законченного целевого кода.

На рисунке 5 показано определение шаблона для генерации Java-кода концепции «Свойство». Шаблон добавляет описание полей, и getter-ов/setter-ов свойства. Этот шаблон – часть генератора, переводящего код с языка структуры на Java.


Рисунок 5. Шаблон генерации Java-кода концепции «Свойство».

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

Использование языка шаблонов можно представить как написание кода на целевом языке, только некоторые части кода «параметризуемы» или «вычисляемы» с помощью макросов. Эта техника сильно упрощает кодогенерацию. Шаблоны могут использоваться и для других задач вроде рефакторинга, оптимизации кода и т.п.

Образцы

Использование поиска по образцу дает мощные возможности поиска моделей как альтернатива языку запросов. Эту возможность можно рассматривать как регулярные выражения для концептуальных моделей. Аналогично шаблонному подходу, генерируется язык шаблонов на основе исходного языка. Язык шаблонов похож на исходный язык, но добавляет возможности определения гибких критериев сложного поиска совпадений в исходной модели. Этот подход можно представить как мощную технику поиска-замены. Опять же, языки образцов используются не только для кодогенерации. К примеру, они очень полезны для написания автоматического инспектора кода в редакторе исходного языка.

Помните, что язык запроса по модели, шаблонные языки и языки образцов поддерживаются мощными редакторами с авто-дополнением, рефакторингом, проверкой ссылок и ошибок и т.п. Сложнейшие запросы, макросы и образцы легко написать. Это доселе невиданная мощность кодогенерации.

Совместное использование языков

Вопросы кодогенерации из предыдущего раздела поднимают интересную тему совмещения языков. Фактически, есть несколько способов добиться этого. В MPS, все концептуальные модели «знают» друг о друге. Поскольку языки также являются концептуальными моделями, это значит, что все языки знают друг о друге и потенциально могут быть взаимосвязаны.

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

Платформы, фреймворки, библиотеки и языки

Чтобы стать полезной, системе, поддерживающей языково-ориентированное программирование, недостаточно только лишь возможностей мета-программирования. Система должна поддерживать все то, на что опирается сегодняшний программист: коллекции, пользовательский интерфейс, работу с сетью и БД… Выбирая язык, программист обращает внимание не только на его суть. К примеру, сила Java не столько в языке, сколько в сотнях фреймворков и API, доступных Java-программисту. Выбирают не язык Java, а всю платформу Java. У MPS тоже будет поддерживающая платформа.

Покуда я не погрузился в детали, коротко обсудим фреймворки. Что такое фреймворк? В традиционном программировании этим термином, как правило, обозначают набор классов и методов, собранных в библиотеку. Попробуем взглянуть на это поближе, и понять, как это выглядит с точки зрения ЯОП.

Зачем объединять классы и методы в библиотеку? Большинство программистов процитирует учителей, говоривших: “повторное использование”. Но остается еще один интересный вопрос. Почему мы повторно используем некоторый набор классов? Ответ: потому что этот набор полезен для решения некоторого класса проблем: построения GUI, доступа к БД, и т.п. Можно сказать, что библиотека классов соответствует некоторой предметной области. Гляньте – вот связь. Библиотеки классов – это недо-DSL! Этот печальный факт огорчает меня неимоверно.

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

Выберем ли мы ограничения этих «почти DSL» или свободу использования настоящих DSL там, где они нужны? Свободу, естественно. Любая библиотека классов – прекрасный кандидат на превращение в полновесный DSL на нашей платформе. К примеру, все библиотеки JDK могут стать DSL на платформе MPS. Для начала понадобятся далеко не все такие DSL, но от остальных зависит мощность и используемость платформы на первых порах. Ниже коротко описаны три наиболее важных языка, поставляемые с MPS: базовый язык, язык коллекций, язык пользовательского интерфейса.

Базовый язык

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

Необходимость в таком языке очевидна. К примеру, желая сложить два числа, мы хотим писать просто «a + b». Не то, чтобы это было нужно везде, но уж точно в некоторой части практически всех программ – там, где это действительно самая подходящая запись.

Название «базовый язык» выбрано потому, что он является основой для многих языков, которым нужна базовая поддержка программирования – переменные, выражения, циклы и т.п. Его можно использовать тремя способами. Можно расширять его при создании своих языков на его основе, можно ссылаться на его концепции в своих программах, можно генерировать свой код на базовом языке. Могут существовать различные генераторы, трансформирующие базовый язык в другие языки – Java, C++ и т.п. Не каждый язык нуждается в базовом, конечно, но во многих случаях он является хорошей отправной точкой.

Язык коллекций

Следующий по важности язык – язык работы с коллекциями. Коллекции нужны везде. Все основные языки программирования так или иначе поддерживают коллекции. К примеру, в Java есть java.util, в C++ – STL. Если бы разные DSL имели собственную поддержку коллекций, они бы превратились в вавилонское столпотворение языков коллекций, несовместимых между собой. Поэтому MPS предоставляет единый язык коллекций, который используют все остальные языки.

Во многих языках программирования коллекции являются не свойством языка, а библиотекой классов (как, к примеру, java.util для Java). Технически, поддержка есть, но она неполноценна, сложна и чревата ошибками. Большая часть Java-кода переполнена ненужными строками повторяющегося кода обработки коллекций. Рисунок 6 показывает пример того, насколько язык коллекций превосходит библиотеку классов. В примере показан алгоритм определения выпуклой оболочки для заданного набора точек. Детально язык коллекций будет описан в будущих статьях.


Рисунок 6. Алгоритм определения выпуклой оболочки, используется язык коллекций

Язык пользовательского интерфейса

Язык пользовательского интерфейса – следующий по важности DSL для нашей платформы. Интересно, что язык редактора, описанный ранее, видимо, может быть использован для создания пользовательских интерфейсов, однако полноценный язык для GUI должен быть более гибким. Преимущества такого языка будут огромными. Код Java Swing – прекрасный пример библиотеки классов, которой лучше было бы быть DSL. У нее все признаки DSL, но ее легко использовать неверно [3], и код Swing крайне запутан. Многие современные среды предоставляют построители GUI для упрощения создания интерфейса. Язык пользовательского интерфейса выполнит эту задачу на более высоком уровне. Подробнее этот язык обсуждается в последующих статьях.

Начало работы с MPS

Мне уже приходилось встречаться со скептическим отношением к ЯОП: «Звучит неплохо, но проект уже давно запущен, и переход к ЯОП сейчас непрактичен», или «Звучит здорово, но начинать новый проект с непроверенной методологией слишком рискованно», или «Звучит здорово, но когда оно заработает в полную силу? ООП понадобилось 20 лет, чтобы стать мейнстримом».

Хорошая новость: вам не нужно кидаться с головой в совершенно незнакомую область; можно сначала попробовать воду пальцем – подходит ли? Можно попробовать использовать небольшие кусочки ЯОП в своем проекте и посмотреть, предоставляют ли они практические преимущества, затем увеличить использование, если понравится. Вот два примера использования ЯОП, которые можно будет попробовать в ближайшем будущем.

Использование MPS в Java-приложениях

На данный момент существует прототип плагина для IntelliJ IDEA, позволяющий включать в проект модели концепций MPS. Эти модели автоматически транслируются в Java-код во время редактирования. Таким образом, вы можете написать часть Java-приложения, используя MPS в том объеме, в котором хотите. Это означает, что вы получаете все возможности MPS, в частности, возможность создания DSL, создания любого расширения языка, использования настраиваемых редакторов с дополнением кода, подсветки ошибок, рефакторинга и т.д. Плагин бесшовно интегрируется с IDEA, позволяя встраивать Java-код в MPS-модели, перемещаться к встроенному или сгенерированному Java-коду, и даже выполнять отладку концепций, аналогично отладке JSP, уже существующей в IDEA. В будущем планируется более тесная интеграция. Система станет новым важным инструментом, доступным Java-программистам, работающим в IDEA.

Обеспечение конфигурации и скриптовых языков

Случай, который мне приходилось наблюдать не единожды. Приложения часто нуждаются в хранении данных о конфигурации, будь то простой файл с опциями или более сложное описание развертывания. Со временем конфигурация становится более сложной, и дело доходит до скриптового языка. Для простых конфигурационных файлов популярен XML. При использовании скриптового языка можно создать свой собственный или использовать один из языков вроде VBScript, Python/Jython, Tcl, Javascript, да хоть бы и Lisp. Каждое из этих решений обладает стандартными недостатками традиционного программирования: долгое время реализации, резкая кривая обучения, малая расширяемость, недостаток поддержки средой.

Вместо этого вы можете создать собственный язык конфигураций/скриптинга при помощи MPS. Пользователи вашего приложения получат легкий в использовании и «умный» редактор для своих скриптов, включая подсветку синтаксиса и ошибок, автодополнение кода, навигацию и т.п. Создание языка и интеграция его в приложение займет небольшой промежуток времени. Рантайм MPS в этом случае можно распространять вместе со своим приложением.

Выводы

Идеи, которые легли в основу ЯОП и MPS, не новы – они обсуждаются уже около 20 лет [1]. Термин «языково-ориентированное программирование» был придуман как минимум 10 лет назад [2]. Новым в моих идеях является то, что они уже проникли в программистское сообщество, и время их пришло. В этой статье я постарался заронить идеи, которые кристаллизуются в новых дискуссиях, мнениях, критике, экспериментах, исследованиях и реальных проектах.

Поэтому я приглашаю вас принять участие в создании новой парадигмы. Вы можете оставить свои комментарии на сайте JetBrains или прислать их по e-mail mps_article@jetbrains.com. Подробности об MPS и ее обновления вы найдете на http://www.jetbrains.com/mps. Смотрите по-новому на сайты, журналы, блоги и книги – с точки зрения ЯОП, и подумайте, насколько все это можно упростить. Подумайте о своих собственных проектах – насколько часто вам приходится создавать и использовать специализированные мини-языки вперемежку с классами и методами. Мне хотелось бы знать, что вы думаете обо всем этом.

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

Существуют и другие проекты, исповедующие схожие подходы, достойны упоминания проекты Intentional Software и Xactium.

Жду ваших откликов!

Благодарности

Я хотел бы поблагодарить Rob Harwood за его помощь в редактировании этой статьи. Хотелось бы также поблагодарить за предложения и комментарии следующих людей: Igor Alshannikov, Florian Hehlen, Jack Herrington, Guillaume Laforge, Vaclav Pech, Thomas Singer, Dmitry Skavish, David Stennett и Timur Zambalayev.

Ссылки

Статьи:

Статьи по Intentional Programming

Книги:

Другие Web-ресурсы:

Интервью об Intentional Programming


Эта статья опубликована в журнале RSDN Magazine #5-2005. Информацию о журнале можно найти здесь