Dsl программирование что это

Разработка приложений на основе DSL и генерации кода

О чем вообще речь

Что такое DSL (Domain Specific Language) и кодогенерация

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

Производительность труда

Если взять разработку приложений, то главной проблемой я считаю низкую производительность, т.е. «количество продукта» на затраченные усилия. В принципе похожая проблема встречается во всех отраслях промышленности, и есть как общие методы решения, так и специфичные. У нас есть много разных вещей для поднятия этой самой производительности — высокоуровневые языки, мощные IDE, continious integration tools, scrum, canban, coffee points, coffee ladies и много чего еще. Но тем не менее разработка продуктов занимает кучу времени. Особенно это заметно, когда то, что нужно сделать можно легко описать словами за несколько минут, а сделать — занимает недели. Существенный разрыв между «что» и «как». «Что делать» — просто и понятно, «как делать» — просто, понятно, но долго. Я хочу сделать «как» — быстро, а в идеале вообще не делать. Короче, декларативный подход.

Уровни абстракции

Есть очень полезное понятие — уровень абстракции. Оно помогает структурировать приложения. Допустим у нас есть приложение для некоторой предметной области. С одной стороны (вверху) есть понятия из этой предметной области, которые так или иначе будут фигурировать в приложении, с другой стороны есть язык программирования общего назначения (внизу), в котором есть байты, типы, методы и тому подобные элементы, не имеющие ничего общего с предметной областью (не будем спускаться ниже до операционной системы, электрических импульсов, транзисторов, молекул, атомов, протонов, кварков. ). Работа программиста как раз и состоит в том чтобы увязать эти два слоя или заполнить область на картинке (левая картинка). Если приложение большое и доменная область достаточно «далеко», то в приложении возникают различные промежуточные уровни абстракции, иначе можно не совладать со сложностью (правая картинка).
Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что этоDsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что этоDsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это
Уровни, конечно, возникают, но возникают они логически. И надо прикладывать определенные усилия, чтобы код тоже поддерживал уровни. Это особенно сложно, если язык один и все запущено в одном процессе. Ведь ничто не мешает вызвать метод из уровня 1 на уровне 3. Да и функции или классы обычно не маркируются уровнем абстракции. Что нам по этому поводу предлагает DSL с кодогеном? Нам по-прежнему надо заполнить ту же область. Соответственно, верхнюю часть заполняем нашим языком, а нижнюю сгенерированным кодом:

В отличие от предыдущего примера уровень здесь непроницаем, т.е. из генерированного кода нельзя вызвать инструкции DSL (особенно если их там нет). Не будем рассматривать случаи, когда генератор делает код на том же DSL… Еще один важный момент здесь — это то, что generated code можно рассматривать как скомпилированный, в том смысле, что он создается автоматически и смотреть в него незачем. При условии, что генератор уже написан (и хорошо протестирован). Т.е. написав язык и генератор к нему можно значительно сузить область приложения. Это особенно ценно при разработке нескольких приложений в этой сфере или при постоянном изменении одного.

Управление «усложнением»

Давайте представим себе ситуацию, которая, как мне кажется, встречается довольно часто. Допустим вы получаете заказ на разработку некоторой системы. Вам приносят идеальную спецификацию и вы придумываете идеальную архитектуру системы где все прекрасно, компоненты, интерфейсы. инкапсуляция и много других не менее прекрасных паттернов. Возьмем конкретный пример — интернет-магазин велосипедов. Вы написали согласно спецификации интернет-магазин и все счастливы. Магазин процветает и задумывается о расширении бизнеса, а именно начать еще торговать скутерами и мотоциклами. И вот они приходят к вам и просят доработать магазин. У вас была прекрасная архитектура, заточенная на велосипеды, но теперь надо перетачивать. С одной стороны скутеры и мотоциклы похожи на велосипеды, и у тех и у тех есть запчасти, аксессуары, сопутствующие товары, но есть и различия.
Система в целом остается такой же, но часть функций должна поддерживать еще новые типы объектов, или должны появиться отдельные функции для новых типов объектов.
Произошло усложнение доменной области, т.е. вместо только велосипедов теперь надо поддерживать велосипеды, скутеры и мотоциклы. Наша системы тоже должна быть усложнена. Я думаю, что в общем случае сложность программной системы соответствует сложности моделируемой системы. При этом существуют минимально возможный уровень сложности при котором все еще можно решить задачу. (Верхнего уровня не существует — можно придумать бесконечно сложное решение для любой проблемы). Я считаю, что надо стремиться к минимальному уровню сложности, так как из всех возможных решений самое простое — самое лучшее. Короче, код должен быть простым.
Вернемся к нашему интернет-магазину. Пусть есть некая функция, которая написана для велосипеда. Теперь она должна работать и для новых типов.

public void process(Bicycle b) <
genericCode
specificForBicycle
>

для этого должен быть specificForMotobike код внутри. Какие есть варианты решения?

Copy/paste

public void process(Motobike b) <
genericCode
specificForMotobike
>
Скопировали метод, заменили специфичный для типа код и все. Просто, но есть проблема. Если надо менять genericCode, то надо менять то же самое в нескольких местах, а это время, ошибки…

If/else

public void process(Object b) <
genericCode
if(b instanceof Bicycle) <
specificForBicycle
> else if(b instanceof Motobike) <
specificForMotobike
>
>

Наставили условий и все готово. Немного лучше, чем copy/paste, но опять есть проблема. А завтра они захотят продавать квадроциклы и придется по всему коду искать такие куски и добавлять еще один else.

Абстрактный метод

abstract void specific()
public void process(Vehicle b) <
genericCode
b.specific()
>

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

DSL и генерация кода

DSL разрабатывается таким образом, что все особенности типов можно описать. В генераторе кода пишутся шаблоны, которые применяются к описанию типов и получается код как в copy/paste
Шаблон:
public void process(«TYPE» b) <
genericCode
«SPECIFIC CODE»
>

DSL вначале или формализованная спецификация

Здесь я подхожу к самому главному. (до этого было вступление 🙂 Как обычно выглядит процесс начала проекта? Пишутся спецификации, рисуются диаграммы, прорабатывается архитектура, этапы проекта. И когда это все сделано, начинают писать код. Спецификации — это документы в свободной форме. Почему бы спецификации не быть формализованной? Моя основная идея — сначала разрабатывать язык описания системы в терминах доменной области. Это будет частично и описание архитектуры, и частично формализованной спецификацией. При этом заказчику будет понятен язык, так как он непосредственно оперирует терминами предметной области, и он тоже сможет принять участие в разработке системы. Идея, конечно, не моя. В литературе такой подход называется Domain-Driven Design (DDD). Я лишь утверждаю, что подход DDD хорошо получается с DSL и генерацией кода.
Формализация означает возможность автоматической обработки. Можно добавить различные проверки на консистентность, непротиворечивость. С другой стороны, разработчики системы имеют готовую формализованную декларацию что должно быть. Остается написать преобразователь в как в работающую систему, те самые кодогенераторы.

Не все так гладко

Средства для разработки DSL и написания генераторов кода

Все что я написал до этого имело бы мало практического смысла без нормальных средств разработки. К счастью такие средства есть. Средства есть разные, но мой выбор Eclipse Xtext. Самое главное, что есть в xtext — интеграция в Eclipse IDE, а именно есть все стандартные свойства — подсветка синтаксиса, ошибки и предупреждения, content assist, quick fix. Это что называется «из коробки». А дальше на что фантазии хватит. Я думаю, что сделаю еще несколько практических постов по теме, если будет интерес.

Источник

О доменных языках

В отличие от универсального языка, такого как C# или UML, доменный язык (DSL) предназначен для выражения инструкций в определенном пространстве или домене.

Хорошо известный домен DSL включает регулярные выражения и SQL. Каждый домен DSL гораздо лучше, чем универсальный язык для описания операций с текстовыми строками или базами данных, но гораздо хуже для описания идей, находящихся за пределами собственной области. Для отдельных отраслей также предусмотрен собственный домен. Например, в телекоммуникационной отрасли языки описания вызовов широко используются для указания последовательности Штатов в телефонном звонке, а в индустрии авиаперелетов Стандартный DSL используется для описания полетом.

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

Планирование путей перехода на веб-сайте.

Схемы подключения для электронных компонентов.

Сети конвейера конвейерных и багажа, обрабатывающие оборудование для аэропорта.

Пользователи вашего DSL создают модели. Модели являются экземплярами DSL. Например, они описывают конкретный веб-сайт или подключение определенного устройства или систему обработки багажа в определенном аэропорту.

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

На следующем рисунке показана небольшая модель в схематическое DSL:

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Возможности DSL

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

После определения DSL его можно распространить другим пользователям, которые могут установить его на своих компьютерах. Пользователи DSL могут создавать и изменять модели в Visual Studio.

Можно также определить команды меню и другие средства, помогающие пользователям редактировать DSL, ограничения проверки, чтобы обеспечить правильное использование DSL, а также шаблоны элементов, помогающие пользователям создавать новые экземпляры. в качестве интегрированного пакета можно включить один или несколько языков dsl с помощью средств и других расширений Visual Studio.

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

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

Разработка Domain-Specific

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

Аспекты разработки графических Domain-Specific

Графический язык, относящийся к домену, должен включать следующие функции:

Модель предметной области

Интеграция с Visual Studio

Notation

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

Модель предметной области

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

Создание артефактов

Одной из основных целей доменного языка является создание артефакта, например исходного кода, XML-файла или других полезных данных. Как правило, изменение в модели означает изменение в артефакте. Можно использовать Инструменты доменного языка для создания артефактов и их повторного создания при изменении модели.

Сериализация

Доменный язык должен быть сохранен в определенной форме, которая может быть изменена, сохранена, закрыта и перезагружена. Инструменты доменного языка использует формат XML, который позволяет определить и настроить сериализацию или сохранение доменного языка.

Интеграция с Visual Studio

поскольку Инструменты доменного языка размещается в Visual Studio, он расширяет множество Visual Studioных окон и элементов управления. Он также позволяет настраивать поведение команд меню, элементов панели элементов и других элементов пользовательского интерфейса.

Вы также можете создать адаптер шины модели для конкретного доменного языка. Этот адаптер позволяет ссылаться на модель и элементы в модели и позволяет писать код, который может получать доступ к экземпляру DSL и обновлять его. используя мощный механизм шины модели, можно писать расширения Visual Studio, работающие с несколькими моделями. Кроме того, можно создавать автономные приложения, работающие с моделями. дополнительные сведения см. в разделе интеграция моделей с помощью Visual Studio Modelbus.

Преимущества разработки Domain-Specific

Доменный язык может предоставить следующие преимущества:

Содержит конструкции, точно соответствующие области проблемы.

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

Позволяет неразработчикам и людям, не знающим домен, понять общую структуру.

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

Упрощает создание прототипа окончательного приложения.

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

Процесс разработки Domain-Specific

Большинство команд разработки программного обеспечения, использующих доменные языки, выполняют следующие шаги для создания и использования их моделей:

Команда различает переменные части домена из частей, которые никогда не изменяются.

Разработчики пишут код для фиксированных частей и оставляют точки расширения для переменных частей.

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

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

Каждый разработчик создает модель, которая применяется к конкретному приложению.

Источник

Универсальный DSL. Возможно ли это?

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Язык предметной области. Не перегружен конструкциями языка общего назначения. При этом позволяет всего несколькими строчками реализовать весьма сложную логику. Все это — DSL.

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

Введение

Есть два способа разработки проекта приложения: сделать его настолько простым, чтобы было очевидно, что в нем нет недостатков, или сделать его настолько сложным, чтобы в нем не было очевидных недостатков. Ч. Э. Р. Хоар (C. A. R. Hoare)

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

Когда-то передо мной стояла задача разработать реализацию промышленного языка (IEC 61131-3) для встраивания в ПО заказчика. В ходе этой работы я увлекся темой интерпретатора-строения и с тех пор в качестве хобби писал интерпретаторы эзотерических и не очень языков. В дальнейшем пришло понимание как использовать самописные интерпретаторы для упрощения повседневной жизни.

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Основная цель вменяемых языков программирования — упростить процесс программирования и чтения программы. Писать на asm проще, чем в машинных кодах, писать на C проще, чем на asm, на C# — еще проще и так далее.

Достигается это в основном за счет самого популярного приема редукционизма — разбиения сложной задачи на простые и осознаваемые компоненты — стандартизации их взаимодействия и определенного синтаксиса.

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

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

В наше время большая часть проектов сводится к комбинированию уже готовых компонент и незначительной низкоуровневой самописной части. Комбинирование компонент обычно делается средствами универсального языка программирования — C#, Java, Python и других. Хотя эти языки и высокоуровневые, они также являются универсальными, а потому обязательно содержат синтаксические конструкции для низкоуровневых операций, создания функций, классов, описание обобщенных типов, асинхронное программирование и многое другое. Из-за этого задача «Сделать раз, сделать два, сделать три» обрастает массой синтаксических конструкций и может раздуться до сотен строк кода и более.

Упростить переиспользование компонент можно, если повторить прием редукционизма, но уже к этим самым компонентам. Достигается это через разработку специализированного языка, который имеет упрощенный синтаксис и служит исключительно для описания взаимодействия этих компонент. Такой подход называется ЯОП (языково-ориентированное программирование), а языки именуются DSL (Domain-Specific Language — язык, специфический для предметной области).

Из-за отсутствия избыточных конструкций всего несколько строк на DSL могут реализовать довольно сложный функционал, что приводит к положительным следствиям: увеличивается скорость разработки, сокращается количество ошибок, упрощается тестирование системы.

Этот подход при удачном применении может существенно увеличить гибкость разрабатываемого продукта за счет возможности написания компактных скриптов, определяющих и расширяющих поведение системы. Приложений данному подходу может быть масса, о чем свидетельствует распространенность данного подхода, ведь DSL повсюду. Распространенный HTML является языком описания документов, SQL — язык структурированных запросов, JSON — язык описания структурированных данных, XAML, PostScript, Emacs Lisp, язык nnCron и много других.

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

При всех плюсах у DSL есть и существенный недостаток — высокие требования к разработчику системы.

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

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

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Выход видится в создании DSL для построения DSL. Здесь я имею в виду не РБНФ, а скорее язык, который может быть изменен встроенными средствами до языка предметной области. Основной помехой в создании гибкого и трансформируемого языка является наличие жестко заданного синтаксиса и система типов. За все время развития компьютерной индустрии было предложено несколько гибких языков без синтаксиса, но дожили до наших дней и продолжают активно развиваться языки Forth и Lisp. Главная особенность этих языков в том, что они благодаря своей структуре и гомоиконности могут за счет встроенных средств менять поведение интерпретатора и при необходимости разбирать синтаксические конструкции, которые не были заложены изначально.

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

Семейство языков Lisp опирается на макросы, которые позволяют вводить DSL при необходимости. А доступ к интерпретатору и ридеру способствует реализации метациклических интерпретаторов с заданными особенностями интерпретации. Например, реализация Scheme lisp Racket позиционируется как среда для разработки языков и имеет из коробки языки для создания веб-серверов, построения GUI-интерфейсов, язык логического вывода и другие.

Такая гибкость делает эти языки хорошими кандидатами на роль движка универсального DSL.

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

Также хочу отметить, что эти языки отлично подходят не только для написания скриптов, но и для интерактивного взаимодействия с системой через REPL. Что, с одной стороны, может быть удобно для отладки, а с другой — выступать в качестве доступного пользователю интерфейса с системой. Есть мнение, что текстовый интерфейс с системой в ряде случаев может быть эффективнее графического, поскольку он значительно проще для реализации, более гибок, позволяет пользователю обобщать типовые операции в функции и так далее. Ярким примером текстового интерфейса может быть Bash. А если язык будет гомоиконным, то его конструкции можно сравнительно легко генерировать и парсить и минимальными силами реализовать поверх интерпретатора графический язык — это может быть полезно, когда целевой пользователь далек от программирования.

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

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

В этом посте я предлагаю создать простой интерпретатор языка «Форт» и покажу, как его адаптировать для решения конкретных задач.

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

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Адресный интерпретатор

Базовым элементом языка «Форт» является слово, которое отделяется от других слов и атомов (чисел) пробелами, концами строк и табуляциями.

Слово имеет такой же смысл и свойства, как и функция из других языков, например C. Слова, зашитые в реализации, то есть реализованные теми же средствами, что и интерпретатор, аналогичны операторам из других языков. Фактически программа на любом языке программирования — не более чем комбинация операторов языка и данных. Поэтому создание языка программирования можно рассматривать как определение операторов и способа их комбинирования. При этом такие языки, как С, определяют разный способ записи операторов, который определяет синтаксис языка. В большинстве языков модификация операторов обычно невозможна — например, нельзя изменить синтаксис или поведение оператора if.

На языке «Форт» все операторы и их комбинации (пользовательские слова) имеют одинаковый способ записи. Форт-слова делятся на примитивные и пользовательские. Можно определить слово, которое будет перегружать примитив, таким образом меняется поведение примитивов. Хотя в действительности переопределенное слово будет реализовано через заданные изначально примитивы. В нашей реализации примитивом будет функция на C#. Слово, определенное пользователем, состоит из списка адресов слов, которые должны быть выполнены. Поскольку есть два вида слов, интерпретатор должен их различать. Разделение примитивов и пользовательских слов осуществляется через те же примитивы, каждое пользовательское слово начинается с операции DoList и заканчивается операцией Exit.

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

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Наша форт-машина состоит из линейной памяти, стека данных, стека возврата, указателя инструкций, указателя слова. Также у нас будет отдельное место для хранения примитивов.

Суть интерпретации заключается в переходе по адресу в памяти и в исполнении инструкции, которая там указана. Весь адресный интерпретатор — сердце языка — в нашем случае будет определен в одной функции Next().

Каждое пользовательское слово начинается с команды DoList, задача которой — сохранить текущий адрес интерпретации на стеке и установить адрес интерпретации следующего слова.

Для выхода из слова используется команда Exit, которая восстанавливает адрес со стека возвратов.

Для наглядной демонстрации принципа работы интерпретатора введем команду, она будет имитировать полезную работу. Назовем ее Hello().

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

Теперь можем составить несложную программу, в нашем случае пользовательский код будет начинаться с адреса 4 и состоять из двух подпрограмм. Первая подпрограмма начинается с адреса 7 и вызывает вторую, которая начинается по адресу 4 и выводит слово Hello на экран.

Чтобы выполнить программу, нужно сначала сохранить в стеке возврата значение 0, по которому адресный интерпретатор прервет цикл интерпретации, и установить точку входа, после чего запустить интерпретатор.

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

Каждое пользовательское слово нашего интерпретатора начинается с примитива DoList, задача которого — сохранить текущий адрес интерпретации и перейти на следующий адрес. Выход из подпрограммы осуществляется операцией Exit, которая восстанавливает адрес со стека возвратов для дальнейшей интерпретации. По сути, мы описали весь адресный интерпретатор. Чтобы выполнять произвольные программы, достаточно расширить его примитивами. Но прежде нужно разобраться с текстовым интерпретатором, который и обеспечивает интерфейс к адресному интерпретатору.

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Текстовый интерпретатор

Язык «Форт» не имеет синтаксиса, программы на нем записываются словами, разделенными пробелами, табуляциями или концами строк. Следовательно, задача текстового интерпретатора — разбивать входной поток на слова (токены), находить для них точку входа, выполнять или записывать в память. Но не все токены подлежат исполнению. Если интерпретатор не находит слово, он пытается его интерпретировать как числовую константу. Кроме того, у текстового интерпретатора есть два режима: режим интерпретации и режим программирования. В режиме программирования адреса слов не исполняются, а записываются в память, таким образом определяются новые слова.

Канонические реализации «Форта» обычно совмещают словарь (словарная статья) и память программ, определяя единый кодофайл в виде односвязного списка. В нашей реализации в памяти будет только исполняемый код, а точки входа слов будут храниться в отдельной структуре — словаре.

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

Флаг Immediate дает интерпретатору указание, что данное слово должно быть исполнено в режиме программирования, а не записано в память. Схематически логику интерпретатора можно изобразить следующим образом: правая рука — YES, левая — NO.

Dsl программирование что это. Смотреть фото Dsl программирование что это. Смотреть картинку Dsl программирование что это. Картинка про Dsl программирование что это. Фото Dsl программирование что это

Для считывания входного потока будем использовать TextReader, для вывода — TextWriter.

Реализация интерпретатора по приведенной выше схеме будет находиться в одной функции Interpreter().

Интерпретация выполняется в цикле, выход из которого осуществляется по достижении конца входного потока (например, конец файла), при этом функция ReadWord вернет пустую строку. Задача ReadWord — с каждым вызовом возвращать очередное слово.

После того как слово было считано, происходит попытка найти его в словаре. В случае успеха возвращается заголовок слова, в противном случае — null.

Проверить, является ли введенное значение числом, можно по первым двум символам. Если первый символ — цифра, то предполагаем, что это число. Если первый символ — знак «+» или «−», а второй — цифра, скорее всего, это тоже число.

Для конвертации строки в число можно использовать стандартные методы Int32.TryParse и Double.TryParse. Но они не отличаются быстродействием по ряду причин, поэтому я использую кастомное решение.

Метод ParseNumber умеет конвертировать как целочисленные значения, так и числа с плавающей точкой, например «1.618».

Исполнение слова происходит аналогично тому, как мы раньше запускали адресный интерпретатор. В случае возникновения исключения будет распечатана трассировка стека адресного интерпретатора.

Когда интерпретатор находится в режиме компиляции и слово не помечено для немедленного исполнения, его адрес должен быть записан в память.

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

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

Мы описали адресный и текстовые интерпретаторы, дальнейшее развитие заключается в наполнении ядра атомами. Различные версии «Форт» имеют разный набор базовых слов, самой минималистичной реализацией будет, пожалуй, eForth, который содержит всего 31 примитив. Поскольку примитив выполняется быстрее составных пользовательских слов, минималистичные реализации «Форта» обычно медленнее многословных реализаций. Сравнение набора слов нескольких версий интерпретаторов можно посмотреть здесь.

Для определения новых пользовательских слов используется два слова ядра — «:» и «;». Слово «:» считывает из входного потока имя нового слова, создает заголовок с этим ключом, в память программ добавляется адрес базового слова doList и интерпретатор переводится в режим компиляции. Все последующие слова будут скомпилированы, за исключением тех, которые помечены как немедленные (immediate).

Завершается компиляция словом «;», которое записывает в память программ адрес слова «exit» и переводит в режим интерпретации. Теперь можно определить пользовательские слова — например, циклы, условный оператор и другие.

Остальные стандартные слова здесь описывать не буду — в сети на соответствующих тематических ресурсах достаточно информации по ним. Для взаимодействия с платформой я определил 9 слов:

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

Пример конфигурации системы по рассылке отчетов

Можно поступить иначе: передать на вход интерпретатора через стек данных уже готовые объекты и дальше взаимодействовать с ними через интерпретатор. Как, например, я делал для восстановления настроек устройства для получения сканов документов, сканер, веб-камера или виртуальное устройство (для отладки или обучения). В данном случае набор параметров, настроек, порядок инициализации разных устройств сильно отличается и решается через форт-интерпретатор тривиально.

Конфигурация генерируется программно, получается что-то вроде этого:

Кстати, похожим способом генерируются скрипты *.ps и *.pdf, ведь как PostScript, так и Pdf по сути являются подмножеством «Форт», но служат исключительно для отрисовки документов на экране или принтере.

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

Скрипт инициализации может быть таким:

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

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

Чтобы вкомпилировать делегат, используем следующую функцию:

Делегат на функцию помещается на стек данных DS.Push(action), затем компилируется слово с вызовом этого делегата. Напомню, что слова, помещенные в квадратные скобки [ ], будут интерпретироваться, а не компилироваться. Слово ‘ Tick ищет следующее слово и помещает его адрес на стек, в нашем случае это слово doLit, которое нужно для того, чтобы во время компиляции делегат как константа был положен на стек. Первая Comma «,» вкомпилирует doLit, вторая вкомпилирует делегат со стэка.

Теперь попробуем написать простенький конфиг, для этого определим класс элемента конфига. Пример взят из реального проекта, где указанные имена имеют семантический смысл:

Интерпретатор конфига делается элементарно — определяются необходимые слова, затем интерпретируется конфиг:

Конечно, я привел самые простые примеры, но этот подход позволяет реализовывать сложные DSL и главное — с минимальными временными затратами.

Итак, мы рассмотрели пример создания и использования простейшего скриптового движка «Форт». Благодаря особенностям языка он может быть использован как универсальный DSL.

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

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

Посмотреть полный исходный код интерпретатора можно здесь, на гитхабе.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *