Современный мир разработки программного обеспечения разный и полон интересных решений. Какие-то из них популярны и де-факто стали стандартом. Предлагаю познакомиться с менее известным инструментом JetBrains MPS на примере ардуино: посмотрим демо и проверим, как можно написать симуляцию человеческим языком с минимумом затрат.
Немного теории и терминов
Что такое JetBrains MPS и при чём тут DSL
MPS — это аббревиатура для Meta Programming System, инструмента для метапрограммирования. Если уйти от деталей и попытаться дать простое определение, то метапрограммирование — это написание программ для написания программ. Можно тут вспомнить аналогию про метаданные, которые являются данными для описания других данных. А если ещё упростить, то метапрограммирование — это про создание DSL.
DSL, он же Domain-Specific Language, он же предметно-ориентированный язык — это язык, который использует термины определённого домена (области). По сути всё есть DSL, только домены отличаются. Например, для Java доменом является язык программирования и такие концепции, как «цикл», «метод», «переменная». Доменом для Gradle является сборка проектов, и там уже свои концепции: «зависимость», «задача», «артефакт». Поэтому под термин DSL можно притянуть почти всё что угодно. Тут могу порекомендовать статью от Federico Tomassetti под названием «The complete guide to (external) Domain Specific Languages».
Нам понадобится термин «модель» (model). DSL состоит из концепций, а пользователь создаёт их экземпляры. Вот эти экземпляры и образуют модели. Мне нравится, как это описано в материале от Microsoft «Understanding Models, Classes and Relationships».
На DSL можно посмотреть и с точки зрения инструмента, который позволяет научить общаться «говорящих» на разных языках. Например, человек на человеческом языке пишет: «У меня есть плата Arduino Uno, а на ней два LED и кнопка», а потом эти конструкции трансформируются в что-то другое, например в JSON-формат, который понятен какому-нибудь симулятору. Чем сложнее конструкции, тем применение DSL становится более оправданным.
Для наших целей подойдёт ресурс с эмулятором Arduino — wokwi. Этот ресурс позволит повторить действия из данной статьи, не устанавливая IDE для ардуино, и даже без него самого. Данный симулятор на вход принимает JSON-конфигурацию и использует её для симуляции. По схожей схеме могут работать и более сложные системы. Например, тестовый стенд для испытания реакции автомобиля под управлением компьютера.
Знакомимся с MPS
Цель этой статьи — продемонстрировать часть возможностей JetBrains MPS и того, как их можно применить. Поэтому писать код мы не будем, а посмотрим на уже готовый код небольшого демонстрационного проекта, подготовленного специально для данного материала. Прежде всего необходимо скачать JetBrains MPS и открыть проект из репозитория, mps-demo.
Открыв проект, мы увидим, что он состоит из разных «модулей». Подробнее про структуру проектов в MPS можно прочитать в разделе документации MPS project structure. MPS выделяет несколько типов модулей, но самые важные для нас — это Language (с иконкой L) и Solution (с иконкой S). Language-модули описывают DSL, а solution-модули используют Language-модули, чтобы в терминах того или иного языка создавать модели.
Каждая модель — своего рода обособленная область. По умолчанию они не видят ничего вокруг, однако можно импортировать объекты из других моделей и подключать разные языки. При подключении в модель языка мы можем создавать экземпляры концепций, которые добавляются данным языком. Например, раскроем в дереве проекта solution-модуль и перейдём курсором на модель sandbox (модели имеют иконку M). Из контекстного меню мы можем открыть Model Properties. Там на вкладке «Used Languages» мы увидим, какие языки подключены, а значит, термины из каких DSL мы можем использовать:
Скетчи и концепты
Закроем окно настроек, вернёмся к модели sandbox и нажмём ALT+INSERT, чтобы увидеть варианты того, что мы можем создать:
Например, воспользуемся WiringLang и создадим новый sketch. Прелесть JetBrains MPS в том, что MPS является своего рода фреймворком для написания DSL. Разработчику не нужно писать IDE, описывать работу выпадающих списков и прочее, а надо в правильном месте правильным образом описать тот или иной аспект языка. Благодаря этому мы можем воспользоваться CTRL+SPACE, чтобы открыть меню и увидеть, что нам доступно:
В случае с коллекциями элементов нажатием Enter в конце элемента мы создадим следующий. Таким образом при помощи WiringLang мы можем написать какой-нибудь пример с сайта wiring.org.co, например Blink:
Если мы встанем на любой объект, который мы создали, и откроем View→ Tool Windows → Inspector, то увидим информацию о том, какой концепт (описание концепции) отвечает за ту или иную фразу, которую мы наблюдаем:
В данном случае описание задержки выражено концептом Delay. В правой части у нас есть ссылка Open Concept Declaration, которая позволяет перейти непосредственно к описанию концепта.
Концепт — это основа, которая описывает сам факт существования какого-то термина и его структуру (из каких полей и ссылок состоит термин). Вокруг концептов описываются другие аспекты, такие как Constraints (ограничение доступных значений, ограничения возможности создавать экземпляры в том или ином месте в модели и прочее) или Editor (как показывать пользователю концепт):
При помощи Show Structure можно увидеть все описанные аспекты.
На примере аспекта Behavior важно сказать, что MPS — в первую очередь про Java. Концептам можно добавлять поведение и описывать это на Java, но с некоторыми дополнительными возможностями вроде своей обёртки над коллекциями (см. Collection Language) и других моментов:
Генератор: и всё превращается в текст
Одним из аспектов языка является генератор. Генераторы позволяют превращать модели либо в текст, либо в модель на другом языке. Предлагаю посмотреть на этот аспект JetBrains MPS несколько подробнее.
Генерация — это процесс преобразования одних моделей в другие. Например, если мы откроем наш ранее написанный скетч Blink и в контекстном меню выберем Preview Generated Text, то мы запустим генераторы и увидим результат. В нашем случае мы увидим текст, соответствующий тому, что указали в модели. Однако это полностью зависит от того, как написан генератор. Модель и конечный результат могут быть очень разными, что станет очевидно чуть позже.
Генерация устроена так, что в конечном итоге всё превращается в текст по тем или иным правилам. Это описано в аспекте TextGen. Например:
Как видно, генератор начнёт обрабатывать экземпляр концепта JSONDocument (см. выбранную вкладку), представляющий JSON-документ. В нём хранится JSON-объект, представленный концептом JsonObject, который является корневым элементом для JSON-структуры. Далее будет вызван TextGen-генератор для JsonObject. В свою очередь JsonObject будет вызывать TextGen для всего, что находится у него внутри. Таким образом, генераторы вызывают друг друга — получается своего рода матрёшка из результатов генераторов.
JSONSupport превращает модели в json-файлы, а WiringLang превращает модели в ino-файлы. Именно эти файлы использует симулятор ардуино wokwi.com. Но основной смысл в другом языке — MpsDemo. Этот язык превращает модели, написанные на нём, в модели JSON и Wiring. В этом случае используется специальный механизм шаблонов:
Чтобы понимать, как это устроено, советую пройти все обучающие материалы MPS из раздела Generator Demos, а также урок Shapes — an introductory MPS tutorial.
Приведу небольшой пример того, как выглядит шаблон для генератора:
Таким образом модель, написанная на MpsDemo, превращается в модели, написанные на JSON и Wiring DSL'ах. Те в свою очередь превратятся в текст.
Предлагаю на этом закончить краткий экскурс в то, как всё устроено изнутри, и рассмотреть один практический пример, ради которого вы читали весь этот текст =)
Симуляция с человеческим языком
Наша задача — описать следующую схему:
Симуляция будет строиться на основе платы Arduino Uno. У нас есть две лампочки красного цвета и одна кнопка. Мы хотим настроить эту схему так, чтобы при нажатии кнопки лампочки загорались.
Чтобы это заработало, нам нужно подключить к земле (GND на плате) катоды и один из контактов кнопки. Аноды подключим к одному пину платы, а ещё один контакт кнопки — к другому пину на плате.
Писать это самому на Wiring и в JSON не очень приятно. Хочется иметь помощь от инструмента: выпадающие списки, фильтрацию, подсвечивание ошибок. Кроме того, хочется описать симуляцию на человеческом языке.
Посмотрим на пример StopSig_test:
Как видно, описание сценария для симуляции написано понятным языком. Кроме того, в MPS можно реализовать перевод редакторов на разные языки (у нас есть английский и русский).
Дополнительные бонусы MPS
Кроме того, благодаря тому, что MPS — это фреймворк и IDE, мы можем воспользоваться различными средствами. Например, при помощи CTRL+SHIFT+I мы можем посмотреть информацию по тому объекту, где в данный момент установлен курсор. Например, тип создаваемых компонентов реализован при помощи специального концепта ComponentDefinition, экземпляры которого хранятся в аспекте, называемом Accessories Models. Это позволяет реализовать что-то вроде справочника. Встанем курсором на тип любого компонента (например, на LED) и нажмём CTRL+SHIFT+I:
Как Вы могли уже догадаться, первая часть нашего описания должна будет стать JSON'ом. Описание же поведения из нижней части сценария станет скетчем из WiringLang.
Вообще генератор при выполнении Rebuild для модели сохраняет результат генерации туда, куда указал генератор. Но проще всего смотреть через контекстное меню и опцию Preview Generated Text. Если мы откроем превью для нашего сценария, то увидим следующее:
Если мы создадим новую симуляцию на wokwi.com и вставим туда результаты генерации, то увидим работающую схему, где нажатие кнопки приводит к тому, что загораются LED.
Таким образом составитель этой схемы не писал ничего в JSON, не писал ничего на Wiring. Он просто написал человеческим текстом сценарий, а наш DSL превратил его в формат, который умеет читать симулятор. DSL выступил своего рода переводчиком между человеком и машиной, унифицировал способ описания сценария, а также предоставил удобный UI. Благодаря MPS нам доступна такая разработческая вещь, как интеграция с системой контроля версий. Кроме того, при необходимости мы можем написать собственный плагин для MPS, о чём подробно говорится в MPS User's Guide: Plugin.
Стандартный компилятор vs. Wokwi
Вы спросите: а чем стандартный компилятор под ардуино не устраивает? Ведь описанную в статье историю с кнопкой и лампочкой можно написать в стандартном IDE. Так вот. Компилятор всем устраивает — wokwi просто позволяет, не имея ни ардуино, ни стандартного IDE, пощупать, как всё можно применить, и понять идею DSL. Это как онлайн-компиляторы под Java, которые помогают быстро посмотреть на код и что-нибудь быстро изучить, не устанавливая себе IDE.
Альтернатива DSL — визуальные редакторы, но они не являются частью данной статьи. При этом DSL — одно из возможных решений, и в качестве альтернативы ему можно предложить визуальный редактор. Например, такой. Выбор остаётся за потребителем и зависит напрямую от того, какой функционал и насколько качественно представлен в том или ином продукте.
Вывод
JetBrains MPS — действительно мощный инструмент. Написание DSL для генерации файлов с инструкциями/конфигурациями — лишь одно из возможных применений. MPS может быть встроен в обычный Java-проект при помощи плагина для системы сборки. MPS может не использовать генерацию, а просто позволять создавать модели и выполнять, зная про эту модель, какой-то Java-код. MPS — гибкий инструмент, он предоставляет широкие возможности, развивается и поддерживается, что тоже является огромным плюсом.
Из минусов — чем глубже погружаешься «в кишки» MPS, тем меньше документации. К MPS нужно привыкнуть, пройдя все стадии от отрицания до принятия. Надеюсь, вам как и мне, понравится этот путь.
Комментарии (7)
Chuvi
17.11.2021 12:03Wiring, Wiring... Так это же C++ самый обычный. Да, без STL. Посмотрите ради интереса как и чем компилируются .ino-файлы. От автоматического вписывания #include на этапе компиляции, C++ не перестаёт таковым быть.
(Это примерно как с Java и Processing. только у Processing-a код в класс заворачивается)
sami777
17.11.2021 15:50+1Ребята, хотите программировать микроконтроллеры, учите Си! Рано или поздно, но вы все равно перейдете с этих "имитаторов" на Си. Но время будет уже потеряно.
veselroger Автор
17.11.2021 17:01+1На всякий случай подчеркну ещё раз: как и было сказано в статье, пример с ардуино был выбран просто потому что он показался наиболее наглядным для идеи "человеческим текстом пишем то, что поймёт компьютер/железка/неведома зверушка". Данный пример чуть ближе к реальности и в отличии от каких-либо хитрых тестовых стендов его можно "повторить дома" имея только интернет и не имея ни ардуино, ни каких-то дополнительных IDE (кроме MPS, но раз уж мы на нём пишем, то без этого никуда). Статья не про то, что мы писали свой DSL для ардуино. Статья про то, что при помощи DSL можно "подружить" человека, который не хочет (и скорей всего не должен в силу своих должностных обязанностей) писать код/JSON/xml/что-то ещё, и программу/железку/что-то ещё, которая хочет читать именно код/JSON/xml/что-то ещё. И как мне кажется, в такой теме важно иметь возможность эту идею пощупать самому, а не просто читать как абстрактные DSL работают с какими-то абстрактными системами. Жаль, что после прочтения статьи это не очень понятно. Надеюсь теперь понятно чуть больше =)
Gordon01
29.11.2021 12:39На си такого не сделаешь: https://docs.rust-embedded.org/book/static-guarantees/design-contracts.html
AVDerov
Начиная с forth много разговоров о метапрограммировании, но в области mcu это не сработало, все пишут на СИ. В моей профессиональной сфере метапрограммирование состоялось в виде matlab, mathematica. Статья неплохая, но баловство это.
forthuser
На Forth (Форт) всё же программируют для MCU.
Достаточно ввести поисковый запрос по слову Forth на Github.
Как пример одного из такого проекта обновившегося на Github hw-cardio-nec русскоязычного пользователя.
Но, да, возможности метапрограммирования Форт, в целом, используются в малом варианте его возможностей и приводят к созданию отдельных языков близких к Форт как например Factor, Rebol и его последователя Red и других.
Местная статья на Хабр 2015 года.
Интервью с Nenad Rakocevic о языке Red, преемнике Rebol
с позиционированием его и для встроенного применения с МК и робототехнике.
Ещё статья к пониманию DSL Универсальный DSL. Возможно ли это?
forthuser
197 Open Source Forth Software Projects
The Top 999 Forth Open Source Projects on Github