Думаю, что нужно логичным образом закрыть внезапно всплывшую тему о DSL-ях (раз и два) своими соображениями о том, имеет ли смысл связываться с DSL и, если имеет, то когда и зачем.
Если говорить коротко, то есть два типа DSL: внутренний/встроенный (internal или emedded DSL) и внешний (external DSL). Внутренний DSL -- это не что иное, как использование возможностей синтаксиса какого-то общеупотребительного языка программирования для создания иллюзии использования другого, нового языка. Я такие вещи делал на Ruby и C++, наслышан, что похожим образом можно создавать встроенные DSL-и на Python-е. Был бы у меня опыт в Lisp-е, наверное, я бы развил эту тему больше, т.к. Lisp просто заточен под то, чтобы на нем под конкретную задачу делали свое подмножество Lisp-а ;) Из языков, с которыми мне приходилось работать, наименее подходит для DSL-естроения Java. И в этом, полагаю, ее сила :) Никакие Равшаты, Джумшуты и eao197 не способны извратиться на Java так, чтобы... Впрочем, это совсем другая история.
Так вот, по своей реализации внутренний DSL довольно слабо отличается от просто библиотеки (классов, функций, макросов или чего-то еще, что поддерживается host-языком). Поэтому, когда я интересовался темой DSL, бытовало мнение, что встроенный DSL -- это не более чем некоторый способ оформления прикладной библиотеки. С этим мнением я не согласен. И считаю, что отношение к DSL как к самостоятельному явлению, более высокоуровневому, нежели библиотека, является краеугольным камнем в вопросе "Имеет ли смысл делать внутренний DSL?"
Исходя из своего опыта могу сказать, что сначала нужно попытаться решить задачу наиболее простыми средствами. Если можно обойтись библиотекой, значит нужно ограничится библиотекой. Но, если становится видно, что при использовании библиотеки решение какой-то задачи упрощается ненамного, то тогда и только тогда можно смотреть в сторону внутреннего DSL.
Одним из критериев здесь может быть декларативность. Преимущество DSL в том, что он повышает декларативность описания какой-то предметной области или ее части. При помощи DSL разработчик кратко и понятно описывает то, что ему нужно, а внутренняя кухня DSL-я делает за него все остальное. По-моему, именно для этого DSL-и и должны использоваться. Т.е. критерий простой: если декларативность DSL-я в разы больше, чем использование библиотеки, то вариант с DSL-ем получает право на жизнь.
Но в декларативности DSL заключена и его слабость. За счет декларативности может потеряться гибкость. А у меня, как правило, она терялась. Для достижения простоты декларативного описания могут быть применены какие-то компромиссы или ограничения. Например, может быть жестко зафиксирован порядок инструкций в DSL-е. Или же какая-то одна инструкция может иметь всего лишь несколько жестко зафиксированных вариантов, расширить список которых прикладной разработчик не может. Со временем список претензий к DSL-ю может вырасти настолько, что расширение DSL-я окажется невозможным и нужно будет придумывать что-то другое: либо новый DSL, либо вообще отказ от DSL-я. Кроме того, отказ от DSL-я может назреть и раньше, если некому его будет дорабатывать. Но при этом не нужно забывать об уже имеющемся прикладном коде, который написан с использованием DSL-ей. Ведь просто так никто этот код переписывать не будет!
В этом плане, на мой взгляд, работа с библиотеками оказывается дешевле. Как правило, библиотеки могут расширяться. В крайних случаях, можно перейти на использование совсем другой библиотеки. При этом со старым кодом можно поступать по-разному: можно просто оставить его как есть и задействовать в проекте сразу две библиотеки для одних и тех же целей (старую для старого кода и новую для нового). Можно API старой библиотеки реализовать как фасад над новой библиотекой, тогда старый код сможет работать с новой библиотекой даже не зная об этом. Если вместо библиотеки было принято решение об использовании DSL-я, то таких возможностей применения нескольких DSL или маскирования нового DSL под старый DSL может вообще не быть.
Еще при работе с внутренними DSL-ями нужно смотреть на то, насколько простыми оказываются потроха самого DSL и оценивать возможные проблемы при его использовании, в том числе и освоение написанного на DSL-е прикладного кода новыми разработчиками. Например, язык Ruby более приспособлен для DSL-естроения, чем C++. То, что в C++ можно сотворить на макросах, как показывает жизнь, лучше не делать :) На этот счет могут быть разные мнения, но я здесь высказываю свою точку зрения, исходя из своего опыта. Чем делать что-то на макросах C++, я бы лучше воспользовался Ruby или Python-ом, из которых бы генерировал вспомогательный C++ код. Для того же Ruby эта проблема не столь актуальна. Хотя и там без чувства меры можно натворить такого, что на первый взгляд выглядит просто и лаконично, то затем, при отладке и развитии прикладного кода вызывает желание обойтись без DSL. Что до Lisp-а, то, вероятно, одна из причин его нишевого статуса как раз в том, что новые программисты не очень благосклонно относятся к сопровождению чужих DSL-поделий ;)
Разговор о внутренних DSL-ях нужно завершить упоминанием еще одного фактора: описание на внутреннем DSL это все-таки код на общеупотребительном, как правило весьма сложном и мощном, языке программирования. И если вам потребуется работа с этим описанием в каком-то инструменте, то вы можете столкнуться с серьезными проблемами. Например, вам нужен GUI-инструмент для конечных пользователей, в котором нужно каким-то образом отображать то, что задекларировано с помощью DSL. Скажем, есть описание C++проекта в виде .rb-файла для системы сборки Mxx_ru. А нужно отобразить содержимое проекта в какой-то IDE. Потребуется либо собственный парсинг ruby-текстов, либо какой-то другой подход к получению информации из проектных файлов.
Еще одна сторона этой проблемы -- это ограничение того, что пользователь может сделать с помощью DSL-кода. Например, rb-файл с описанием проекта для Mxx_ru -- это обычная Ruby-программа. Соответственно, пользователь может написать ее так, что помимо сборки проекта Mxx_ru будет удалять какие-то файлы, пытаться подбирать чужие пароли, DDOS-ить сайты или вытворять что-то еще. Если вы отдаете свой внутренний DSL на использование сторонним пользователям, вам нужно будет серьезно подумать о том, нужно ли ограничивать их возможности. И если нужно, то сможете ли вы это сделать вообще.
И вот как раз здесь можно перейти к разговору о внешних DSL-ях. Трудоемкость их создания, очевидно, много выше, чем для внутренних DSL. Но есть три фактора, которые заставят вас сделать выбор в пользу внешних DSL:
- необходимость автоматического разбора/обработки или генерации DSL-описаний. Опять же пример с описанием C++ проекта: если оно хранится в виде XML-файла, то реализовать инструменты для автоматической обработки этих файлов будет проще, чем для проектов Mxx_ru или SCons;
- необходимость очень жесткого контроля за тем, что может делать пользователь посредством DSL. Если в DSL-файле должно быть только описание C++ проекта, то в XML-файле это ограничение выдержать гораздо проще, чем в .rb-файле для Mxx_ru или .py-файле для SCons-а;
- необходимость работы с DSL-описаниями специалистов в своей прикладной области, которые могут иметь очень слабое представление о программировании. Например, если ваш DSL описывает последовательность фильтров, которые нужно применить к цифровому изображению или аудиозаписи, то вряд ли пользователь DSL-я окажется опытным Ruby/С++/Scala/Haskell/Lisp разработчиком.
Т.е. не смотря на намного большую трудоемкость создания внешних DSL, вы можете столкнуться с ситуациями, когда у вас нет другого выбора. Нужно просто брать и делать внешний DSL.
И здесь вопрос будет в том, насколько специализированный синтаксис DSL вам нужен. Если вы хотите иметь оптимально заточенный под конкретную прикладную область DSL, то вам придется взять на себя решение проблем лексического и синтаксического анализа. В принципе, все не так печально, поскольку есть куча инструментов разного уровня продвинутости, раскрученности и стоимости, как для того, так и для другого. Мне в свое время хватало flex-а и bison-а, хотя сейчас бы я смотрел в сторону Ragel и CoCo/R или ANTLR. Впрочем, тема лексического/синтаксического анализа настолько увлекательна, что есть опасность погрузившись в нее забыть про изначальную цель и заняться только играми с разными типами парсеров :) Поэтому, если вы не специалист в этой специфической области и занялись разработкой синтаксиса для своего DSL, то нужно трезво отдавать себе отчет о том, что в каждом генераторе парсеров есть свои проблемы, но ваша задача в том, чтобы всего лишь сделать свой DSL с учетом ограничений конкретного инструмента. И не комплексовать по этому поводу :)
Для себя я пришел к выводу, что вместо специализированного синтаксиса, заточенного под конкретную задачу, лучше использовать какой-то более универсальный (возможно более многословный) синтаксис. Особенно на теговой структуре. Хороший пример такого -- XML. Но только пример. Из-за своей многословности XML не есть хороший выбор для DSL-ей, описания на которых будут делать a) люди и b) простыми средствами (вроде обычных текстовых редакторов). Лично мне XML категорически не нравится и я остановился на подмножестве синтаксиса языка программирования Curl (подробнее об этом я писал здесь).
Теговая структура DSL в отличии от заточенного под задачу синтаксиса хороша простотой расширения языка со временем. Если DSL окажется успешным и востребованным, то вам (а может уже и не вам, а вашим последователям) обязательно придется его расширять. И делать это с тегами, на мой взгляд, много проще: можно добавлять/удалять дочерние теги или атрибуты, несложно писать инструменты для автоматической трансформации старых описаний в новые. Сложно передать разницу в сложности расширения bison-грамматики для расширения какой-то одной конструкции DSL и в добавлении еще одного дочернего тега в теговом DSL. Она просто колоссальна :)
Но вопрос выбора синтаксиса далеко не праздный и не простой. Сложно представить себе запись регулярных выражений или EBNF-грамматики посредством теговой структуры. Пытаясь выиграть на последующем сопровождении и выбрав теговую структуру легко потерять главное преимущество DSL -- простоту декларативности.
Нужно ли вообще связываться с внешними DSL при всей их трудоемкости? It depends, как говорится. Но не так все страшно. Даже конфигурационные файлы, в принципе, являются DSL-ями. А создавать их, особенно при наличии привычных инструментов, не так уж и сложно.
Кроме того, на любой философский вопрос "А нужно ли...?" всегда можно дать такой же философский ответ "Чтобы вы не сделали, вы потом пожалеете" ;) При наличии опыта, навыков и здравого смысла, DSL-естроение вполне себе оправдано. Важно лишь понимать, что внутренний DSL обойдется вам дороже, чем библиотека. Внешний DSL обойдется еще дороже, чем внутренний. Имеется в виду трудоемкость реализации библиотеки и DSL-ей. И если вы можете достичь результата дешевым способом, то зачем платить больше? :)
В пользу DSL-ей же работает то, что не все можно сделать библиотекой. Взять, например, описание регулярных выражений или описание программных интерфейсов (IDL). В этих случаях альтернативы DSL-ям нет. И если вы столкнулись с чем-то подобным, то и выбора у вас не будет, нужно будет браться и делать DSL. Ну а как его делать -- это уже каждому решать самостоятельно, исходя из своего опыта, бюджета и эстетических пристрастий :)
PS. Напоследок дам маленький вредный совет: если вы впервые взялись за создание внешнего DSL, то не спрашивайте советов на публичных программерских форумах. Там сидит столько "экспертов" в области парсинга (да и вообще всего на свете), что вы обязательно получите кучу противоречащих друг другу, а то и здравому смыслу, советов, примеров и мнений. Просто возьмите и сделайте как сможете. Получив опыт затем сможете понять, о чем именно вам говорят другие люди. В крайнем случае в частном порядке спросите совета у одного-двух людей, которых вы цените как специалистов и уже знаете, насколько точно/сильно их оценки коррелируют с действительностью.
Комментариев нет:
Отправить комментарий