предисловие
В последние годы слово DSL стало часто появляться в сообществе разработчиков интерфейсов, что во многом связано с изменениями в среде:
- Дизайн уровня представления современных фреймворков, таких как React, Vue и Angular, часто тесно связан с DSL.Благодаря этим превосходным работам мы можем получить некоторые практические рекомендации.
- Цепочка инструментов перевода и компиляции языков, связанных с внешним интерфейсом, имеет тенденцию к зрелости, напримерbabel,postcssТакие инструменты могут помочь разработчикам участвовать в процессе создания языка с минимальными затратами, расширяя подключаемые модули.
- Начали распространяться инструменты генерации парсеров сообщества, такие какjison,PEG.jsи т. д., могут помочь разработчикам быстро внедрять новые языки программирования (обычно внешние DSL, такие как шаблоны).
Хотя у нас и начали расцветать практики "скиллов", но возникли и некоторые недоразумения или мифы.Например, чисто технические вопросы, такие как DSL и транскомпиляция, приравниваются, например, к внутренним границам DSL и библиотек (интерфейсов), так далее.,DSLПоэтому оно стало словом, о котором все говорят, но оно очень незнакомо.
В то же время авторитетные работы на рынке, такие как «Domain-Specific Languages» Мартина Фаулера, как правило, склоняются к ответам «Дао», но они полны таких вопросов, как «Контролер палаты мисс Грант» и «Высокие ценные бумаги». Непонятный случай акклиматизации к фронтенд-разработчикам. На самом деле ежедневная работа фронтенда уже неразрывно связана с DSL, вам как разработчику больше не нужно изучать DSL через эти рывковые кейсы.
В силу специфики опыта работы автор данной статьи накопил некоторый практический опыт работы с front-end DSL (в основном внешними DSL).проект с открытым исходным кодомТак же есть некоторые проявления в сообществе, и у автора тоже есть некоторые несистемные ответы в сообществе, типа"Как написать инструмент компиляции, похожий на LESS". На этот раз я постараюсь подробно обсудить DSL с точки зрения фронтенд-разработки.«Трудно сказать»проблема.
Из-за нехватки места статья будет разделена на две части:
- Часть 1: Введение в DSL + внутренний DSL;
- Часть 2: Краткий обзор практики внешнего DSL + внешнего DSL.
Введение в DSL
Как и многие концепции в компьютерной области, DSL на самом деле является сначала практикой, а затем определением.
DSL - это «язык домена», который обычно переводится как «язык домена» на китайском языке.Языки доменаВ книге есть определение:
дляконкретное полеразработан, сограниченная выразительностьизЯзык программирования
Разработка языков программирования на самом деле представляет собой процесс непрерывной абстракции, например, от машинного языка к языку ассемблера, а затем к языкам высокого уровня, таким как C или Ruby:
Как показано на рисунке выше, язык ассемблера заменяет коды операций машинных инструкций мнемоникой, что значительно повышает удобочитаемость и удобство сопровождения машинного языка. Но по сути это все еще низкоуровневый язык программирования для аппаратных систем, таких как процессоры и регистры. Появление языков высокого уровня решает эту проблему, и это действительно разрывает прямую ассоциацию с набором машинных инструкций.Вышеуровневые абстрактные операторы (управление потоком, циклы и т. д.) и структуры данных больше приближаются к естественным язык и математические формулы для завершения работы по кодированию.Значительно повысить эффективность разработки программы.
Но на уровне языка высокого уровня выигрыш в эффективности, обеспечиваемый абстракцией, кажется, имеет потолок. Будь то от C до Java или языки программирования с более высокой степенью абстракции, полученные из различных парадигм программирования, все они решают общие проблемы программирования.Все они имеют достаточную абстракцию процессов и абстракцию данных, что приводит к большому количеству концепций, а затем повлиять на эффективность программирования.
На самом деле языковых возможностей для обработки задач в некоторых проприетарных областях не так уж и много.DSL является решением этого противоречия.Это языковой инструмент для решения конкретных задач.Например, для написания документов есть уценка, а для сопоставления строк есть RegExp, управление миссией имеет make, gradle, поиск данных имеет SQL, кодирование в веб-стиле имеет CSS и так далее. По сути, это то же самое, что и наше решение многих проблем программной инженерии.Ограничение границ предметной области блокирует сложность и повышает эффективность программирования..
Давайте сначала возьмем простой пример, такой как выражение2 недели назад:
Решение первое
new Date(Date.now() - 1000 * 60 * 60 * 24 * 7 * 2);
Решение второе
2 weeks ago
Решение третье
(2).weeks().ago();
Решение 1 — это решение, которое соответствует общему мышлению программирования, но даже мы, программисты, не можем с первого взгляда увидеть его значение.
Решение 2 и Решение 3 на самом деле представляют собой два разных типа DSL - внешний DSL и внутренний DSL. Они, очевидно, более интуитивно понятны (спросите свою девушку, если вы не верите в это), но они не могут работать напрямую. Запуск их в JavaScript будет получить совершенно другую ошибку:
-
2 weeks ago
получитеUncaught SyntaxError: Unexpected identifier
изГрамматические ошибки. -
(2).weeks().ago()
получитUncaught TypeError: 2.weeks is not a function
изошибка типа времени выполнения.
На самом деле мы можем видеть, что они существенно отличаются от типа ошибки.
Кратко о внешнем DSL
Второе решение называетсяВнешний DSL, это независимый язык программирования, он требует реализации собственных инструментов компиляции из парсера, а стоимость реализации высока. Но его синтаксис является более гибким и более простым для удовлетворения выразительных потребностей пользователей.
Прямым аналогом внешнего DSL является GPPL, который обычно не требует полноты по Тьюрингу из-за меньшего количества ограниченных синтаксических функций, поэтому сложность его реализации ниже, чем GPPL.
GPPL расшифровывается как «язык программирования общего назначения», также известный как языки программирования общего назначения, такие как наш часто используемый JavaScript, которые предназначены для решения задач программирования общего назначения.
Обычно используемые шаблонизаторы внешнего интерфейса, такие какmustacheА синтаксис JSX, поддерживаемый React и Vue, — это все внешние DSL.
пример с усами:
<h2>Names</h2>
{{#names}}
<strong>{{name}}</strong>
{{/names}}
Это намного эффективнее, чем собирать струны вручную.
Краткий обзор внутреннего DSL
Третье решение, которое мы называемВнутренний DSL (встроенный DSL или внутренний DSL), который представляет собой специальный DSL, построенный поверх других основных языков (обычно GPPL).Он разделяет инфраструктуру, такую как инструменты компиляции и отладки, с основным языком, который дешевле изучать и легче интегрировать. Он синтаксически гомологичен основному языку, но требует дополнительной инкапсуляции во время выполнения.
Вы также можете рассматривать внутренний DSL как особый стиль инкапсуляции интерфейса для конкретной задачи, например, jQuery можно рассматривать как внутренний DSL для манипулирования DOM.
Синтаксическая гибкость и синтаксический шум внутреннего DSL часто зависят от выбора основного языка, и в этом примере мы сосредоточимся на JavaScript.
syntactic noise is syntax within a programming language that makes the programming language more difficult to read and understand for humans.
Короче: больно смотреть, больно писать.
Наконец, давайте посмотрим на взаимосвязь между внутренним DSL и внешним DSL и общим языком GPPL:
Определение внутренней DSL всегда было в центре внимания сообщества.Чтобы понять, что такое внутренняя DSL, давайте сначала ознакомимся с типичным стилем построения внутренней DSL.
Руководство по внутреннему стилю DSL (описание JavaScript)
На самом деле есть несколько применимых стилей для создания внутренних DSL с помощью JavaScript.
Стиль 1: Каскадный подход
Каскадные методы являются наиболее распространенным шаблоном для внутренних DSL, давайте начнем с манипулирования собственным DOM в качестве отрицательного примера:
const userPanel = document.querySelector('#user_panel');
userPanel.addEventListener('click', hidePanel);
slideDown(userPanel); //假设这是一个已实现的动画封装
const followButtons = userPanel.querySelectorAll('button');
followButtons.forEach(node => {
node.innerHTML = 'follow';
});
Я считаю, что каждому сложно сразу увидеть, что было сделано, но если мы используем древний фреймворк jQuery для достижения эквивалентного эффекта:
$('#user_panel')
.click(hidePanel)
.slideDown()
.find('button')
.html('follow');
Нетрудно понять, что это значит:
- оказаться
#user_panel
узел; - Установите, чтобы скрыть его после клика;
- Расширьте нисходящий динамический эффект;
- Затем найдите все узлы кнопок под ним;
- Заполните эти кнопки последующим содержанием.
Суть стиля цепного вызова, такого как каскадные методы, заключается в том, чтоВызов больше не рассчитан на конкретное возвращаемое значение, а напрямую возвращает следующий контекст (обычно сам), чтобы добиться каскадных вызовов.
Стиль 2: Каскадные конвейеры
Каскадный конвейер - это просто специальное применение каскадного метода, типичный случайgulp:
Gulp — это инструмент управления задачами сборки, похожий на make, который абстрагирует файлы в файл с именемVinyl(Формат виртуального файла), абстрактный файл использует метод канала для прохождения через преобразователь, в свою очередь, для выполнения задачи.
gulp.src('./scss/**/*.scss')
.pipe(plumber())
.pipe(sass())
.pipe(rename({ suffix: '.min' }))
.pipe(postcss())
.pipe(dest('./css'))
Многие люди почувствуютgulp
Знакомый, поскольку его философия дизайна основана на конвейерах в командной строке Unix, приведенный выше пример можно напрямую сравнить со следующей командой:
cat './scss/**/*.scss' | plumber | sass | rename --suffix '.min' | postcss | dest './css/'
Вышеупомянутая абстракция для конвейера также может использоваться для построения DSL в обычном каскадном вызове, таком какchajs:
cha()
.glob('./scss/**/*.scss')
.plumber()
.sass()
.rename({ suffix: '.min' })
.postcss()
.dest('./css')
Вышеупомянутое является просто аналогией синтаксиса для DSL, chajs не обязательно имеет
plumber
и другие функциональные модули.
За счет сокращения несколькихpipe
, код явно уменьшился, но беглости не стало больше.
Второйchajs
Стиль требует, чтобы эти методы расширения были зарегистрированы в экземпляре, что увеличивает стоимость интеграции, и эти коды интеграции также влияют на беглость DSL.
cha
.in('glob', require('task-glob'))
.in('combine', require('task-combine'))
.in('replace', require('task-replace'))
.in('writer', require('task-writer'))
.in('uglifyjs', require('task-uglifyjs'))
.in('copy', require('task-copy'))
.in('request', require('task-request'))
Напротив,gulp
Унифицировать расширение как некое внешнееtransformer
, явно предназначенный для большей элегантности.
Стиль 3: Каскадные свойства
Каскадный способ как в начале статьи(2).weeks().ago()
, на самом деле недостаточно лаконично, явный синтаксический шум,(2).weeks.ago
Очевидно, что это лучший способ, мы можем сделать это через статический прокси атрибута, ядроObject.defineProperty()
, который может захватить собственностьsetter
а такжеgetter
:
const hours = 1000 * 60 * 60;
const days = hours * 24;
const weeks = days * 7;
const UNIT_TO_NUM = { hours, days, weeks };
class Duration {
constructor(num, unit) {
this.number = num;
this.unit = unit;
}
toNumber() {
return UNIT_TO_NUM[this.unit] * this.number;
}
get ago() {
return new Date(Date.now() - this.toNumber());
}
get later() {
return new Date(Date.now() + this.toNumber());
}
}
Object.keys(UNIT_TO_NUM).forEach(unit => {
Object.defineProperty(Number.prototype, unit, {
get() {
return new Duration(this, unit);
}
});
});
Вставив приведенный выше код в консоль, введите(2).weeks.ago
Попробуйте, вы увидите, что каскадные свойства могут иметь более лаконичное представление, чем каскадные методы, но при этом теряют гибкость на уровне параметров.
Некоторые могут задаться вопросом, почему нет
2.weeks.ago
, который является одним из "Feature". Единственное решение — использовать базовый язык с меньшим количеством синтаксического шума, такой как CoffeeScript.
В стиле DSL, будь то каскадный метод, каскадный конвейер или каскадное свойство, сущностью является стиль цепного вызова.Основой цепного вызова является передача контекста, поэтому каждый разСоответствует ли возвращенный объект вызова разуму пользователяЭто важная основа для успеха проектирования DSL.
Стиль 4: вложенные функции
В разработке также находятся некоторые сценарии иерархической абстракции, такие как создание деревьев DOM.Ниже приведены примеры чисто императивного использования API-интерфейсов DOM для построения:
const container = document.createElement('div');
container.id = 'container';
const h1 = document.createElement('h1');
h1.innerHTML = 'This is hyperscript';
const list = document.createElement('ul');
list.setAttribute('title', title);
const item1 = document.createElement('li');
const link = document.createElement('a');
link.innerHTML = 'One list item';
link.href = href;
item1.appendChild(link1);
const item2 = document.createElement('li');
item2.innerHTML = 'Another list item';
list.appendChild(item1);
list.appendChild(item2);
container.appendChild(h1);
container.appendChild(list);
Этот способ написания немного непонятен, и трудно сразу увидеть окончательную структуру HTML.Как построить внутренний DSL, чтобы плавно решить этот уровень абстракции?
Некоторые люди пытаются реализовать это аналогично цепным вызовам, например:concat.js:
builder(document.body)
.div('#container')
.h1().text('This is hyperscript').end()
.ul({title})
.li()
.a({href:'abc.com'}).text('One list item').end()
.end()
.li().text('Another list item').end()
.end()
.end()
Это кажется намного лучше, чем императивный способ написания, но при построении такого DSL возникает довольно много проблем:
- Поскольку ключом к цепочке вызовов является передача контекста, требуются дополнительные уровни абстракции.
end()
Всплывающее действие реализует переключение контекста. - Хорошая читабельность зависит от ручного отступа, а автоматический отступ редакторов имеет тенденцию нарушать эту гармонию.
Таким образом, общая абстракция иерархической структуры редко использует стиль цепных вызовов для построения DSL и будет использовать более простыевложенные функцииреализовать.
Мы открываем проект с другим пепломDOMBuilderНапример:
Отпусти здесь
with
проблема собственного использования
with(DOMBuilder.dom) {
const node =
div('#container',
h1('This is hyperscript'),
ul({title},
li(
a({herf:'abc.com'}, 'One list item')
),
li('Another list item')
)
}
Вы можете видеть, что иерархическая абстракция более гибкая для использования вложенных функций.
Если описать на CoffeeScript, синтаксический шум можно уменьшить до уровня, близкого кpugСинтаксис этого внешнего DSL:
div '#container',
h1 'This is hyperscript'
ul {title},
li(
a href:'abc.com', 'One list item'
)
li 'Another list item'
CoffeeScript — это язык, скомпилированный в JavaScript.Он направлен на то, чтобы удалить шлак дизайна языка JavaScript и добавить много синтаксического сахара, который влияет на эволюцию многих последующих стандартов JavaScript.Он выполнил свою историческую задачу и постепенно исчез.
Вложенные функции по существу подразумевают переключение контекста, которое необходимо обрабатывать в цепочке вызовов в операции вложения функций, поэтому это очень подходит для сценариев иерархической абстракции.
Кроме того, применение вложенных функций в DSL похоже на дерево синтаксического анализа, поскольку оно соответствует идее генерации синтаксического дерева и часто может быть напрямую сопоставлено и преобразовано в соответствующий внешний DSL, такой как JSX:
<div id='container'>
<h1 id='heading'> This is hyperscript </h1>
<ul title={title} >
<li><a href={href} > One list item </a></li>
<li> Another list item </li>
</ul>
</div>
вложенные функцииЭто не панацея, это, естественно, не подходит для чувствительных к последовательности сценариев, таких как процесс и время.
Если вы измените каскадный конвейер стиля 2 на вложенные функции:
Логика выполнения явно несовместима с порядком чтения, и это увеличит нагрузку на запись (уделяя внимание логике открытия и закрытия), что сильно влияет на беглость чтения и записи.
Стиль 5: Литералы объектов
Многие DSL в отрасли похожи на файлы конфигурации, такие как JSON,YAMLи другие внешние DSL, они очень выразительны в представлении вложенных данных.
Кроме того, в JavaScript есть функция, подходящая для построения DSL в этом сценарии, т. е.буквальный объект, по факту,JSON(полное название JavaScript Object Notation) является производным от этой функции и стал стандартным форматом обмена данными.
Например в проектеpuer, файл конфигурации маршрутизации выбрал литералы объектов JS вместо JSON:
module.exports = {
'GET /homepage': './view/static.html'
'GET /blog': {
title: 'Hello'
}
'GET /user/:id': (req, res)=>{
res.render('user.vm')
}
}
Поскольку JSON имеет естественный недостаток, заключающийся в том, что он требует сериализации, что сильно ограничивает его выразительность (но также делает его самым популярным форматом обмена данными между языками), например, в последнем из приведенных выше примеров также были введены функции, хотя это «нечисто». с точки зрения DSL, но функциональность — это шаг вперед. Вот почему некоторые DSL, связанные с задачами сборки (make, rake, cake, gradle и т. д.), почти все являются внутренними DSL.
Кроме того, из-за существования значения ключа объекта объектные литералы также могут улучшить читаемость параметров, таких как:
div({id: 'container', title: 'This is a tip' })
// CoffeeScript Version
div id: 'container', title: 'This is a tip'
Очевидно, более читаемый, чем следующий пример с меньшим количеством слов:
div('container', 'This is a tip')
Построение DSL не так просто, как это возможно, и ключом к этому является повышение беглости речи.
Литералы объектов имеют строгую структуру и обычно используются только для сценариев абстракции данных, таких как конфигурация, и не подходят для сценариев абстракции процессов.
Стиль 6: Динамические прокси
Типичным недостатком конструкции внутренних DSL, перечисленных выше, является то, что они являются статически определенными свойствами или методами, а не динамическими.
как указано выше[Стиль 4: вложенные функции]упоминается вconcat.js, все аналогичноdiv
,p
и т.д. методыстатическое именованное определениеиз. а на самом деле потому чтоcustom elementsСуществование фич, этот статичный и исчерпывающий метод явно изрыт, не говоря уже о том, что сам стандарт html постоянно добавляет новые теги.
А во внешнем DSL такой проблемы нет, как я писал ранееregularjs/regular, его встроенный механизм шаблонов помещает что-то вроде/<(\w+)/
Текст соответствует униформеTAG
лексический элемент, чтобы можно было избежать истощения.
Для достижения этой функции внутренний DSL в значительной степени зависит от возможностей метапрограммирования основного языка.
Функции, которые Ruby, как типичный базовый язык, часто использует для демонстрации своих мощных возможностей метапрограммирования:method_missing
, этот метод может динамически получать все неопределенные методы, и наиболее непосредственной функцией является динамическое именование методов (или метаметодов), что может решить проблему, связанную с тем, что все вышеупомянутые внутренние DSL именованы и определены статически.
К счастью, в JavaScript есть более мощная языковая функция, котораяProxy, который может проксировать приобретение собственности, чтобы решить исчерпывающую проблему concat.js, описанную выше.
Ниже приведен не полный код, а простая демонстрация.
function tag(tagName){
return {tag: tagName}
}
const builder = new Proxy(tag, {
get (target, property) {
return tag.bind(null, property)
}
})
builder.h1() // {tag: 'h1'}
builder.tag_not_defined() // {tag: 'tag_not_defined'}
Благодаря прокси-серверу JavaScript обладает мощными возможностями метапрограммирования.В дополнение к возможности легко моделироватьСамодовольная функция Ruby method_missingКроме того, может быть многоДругие возможности динамического прокси, которые являются важными инструментами для реализации внутренних DSL.
Стиль 7: Лямбда-выражения
На рынке существует большое количество библиотек запросов, использующих стиль цепочки, который очень близок к тому, как написан сам SQL, например:
const users = User.select('name')
.where('id==1');
.where('age > 1');
.sortBy('create_time')
чтобыid==1
После того, как выражение преобразовано в исполняемое условие фильтра, мы должны реализовать полный синтаксический анализатор выражений, чтобы, наконец, скомпилировать эквивалентную функцию.
function(user){
return user.id === 1
}
Стоимость реализации очень высока, и эту потребность можно решить с меньшими затратами, используя лямбда-выражения.
const users = User.select('name')
.where(user => user.id === 1);
.where(user => user.age > 20);
.sortBy('create_time')
На самом деле, такие варианты использования уже существовали, например, на основеC#
изLINQ(Language-Integrated Query), что также является типичным случаем, наиболее часто активным во внутреннем технологическом кругу DSL.
var result = products
.Where(p => p.UnitPrice >= 20)
.GroupBy(p => p.CategoryName)
.OrderByDescending(g => g.Count())
.Select(g => new { Name = g.Key, Count = g.Count() });
Лямбда-выражения интуитивно понятны и легко читаются.отложенное исполнениеВозможность логического выражения позволяет избежать дополнительной работы по синтаксическому анализу, но сильно зависит от поддержки языковых функций хоста (анонимная функция + представление стрелки), а также вносит определенный синтаксический шум.
Стиль 8: Абстракция естественного языка
Абстракция естественного языка заключается в том, чтобы спроектировать грамматику DSL таким образом, чтобы он был ближе к естественному языку.Основная логика его работы заключается в том, что эксперты в предметной области в основном являются естественными людьми, такими как вы и я, и легче принять грамматику естественного языка. .
Суть абстракции естественного языка — это некий синтаксический сахар, отличный от синтаксического сахара общей GPPL. Синтаксический сахар DSL не обязательно является самым кратким, но вместо этого добавляет некоторый «избыточный» нефункциональный синтаксический словарь.
Например, это открытый исходный код в команде облачной музыки.svrx(Server-X)В проекте (подключаемая платформа dev-сервера) маршрутизация является часто используемой функцией, для которой мы разработали наборВнутренний DSLДля облегчения использования разработчиками, как показано в следующем примере:
get('/blog/:id').to.send('Demo Blog')
put('/api/blog/:id').to.json({code: 200})
get('/(.*)').to.proxy('https://music.163.com')
вto
Это нефункциональное слово, но оно упрощает понимание и использование всего утверждения обычными людьми (включая нас, программистов, конечно).
Благодаря абстракции естественного языка преимущества внутреннего DSL полностью используются в сценариях модульного тестирования, например, если мы используем что-то вродеassertМетод assert для модульного теста может выглядеть так:
var foo = '43';
assert(typeof foo === 'number', 'expect foo to be a number');
assert(
tea.flavors && tea.flavors.length === 3,
'c should have property flavors with length of 3'
)
Есть несколько важных вопросов, которые необходимо оптимизировать:
- Императивные утверждения не интуитивно понятны;
- Для удобочитаемости отчета добавлены дополнительные подсказки (например,
expect foo to be a number
).
Если этот случай основан наchaiКогда дело доходит до написания, читабельность сразу же повышается:
var foo = '43'
// AssertionError: '43' should be a 'number'.
foo.should.be.a('number');
tea.should.have.property('flavors').with.lengthOf(3);
Можно обнаружить, что тестовые случаи стали легче читать и писать, а когда утверждение терпит неудачу, оно автоматически собирает более понятное сообщение об ошибке в соответствии с состоянием, сгенерированным цепным вызовом, особенно в сочетании с тестовыми средами, такими как mocha, это может быть создание интуитивно понятных тестовых отчетов напрямую:
Добавляя вспомогательные грамматики, подобные естественному языку (глагол, имя, введение, наречие и т. д.), операторы программы можно сделать более интуитивно понятными и простыми для понимания.
резюме стиля
В этой статье не рассматриваются все стили внутренней реализации DSL (например, есть некоторые геймплеи на основе декораторов), а перечисленные стили не являются серебряными пулями, у всех есть свои применимые сценарии, и между ними есть дополнительный эффект.
Некоторые мифы о внутренних DSL
Введя некоторые идиоматические стили выше, мы получили некоторое представление о внутреннем интерфейсе DSL.В этом разделе будет подробно рассмотрен вопрос «Почему?»:
Почему JavaScript был выбран в качестве основного языка
Как видно из случая стиля, хост-язык напрямую определяет верхний предел «грамматической» оптимизации внутреннего DSL. Как ROR для Ruby, так и Gradle для Groovy, типичный предварительный выбор больше, чем затраченные усилия. Самый удобный язык для фронтенд-разработки, JavaScript, на самом деле имеет большое преимущество при создании внутренних DSL из-за множества языковых функций:
- Он основан на типах данных и управлении памятью языка Java с высокой степенью абстракции.
- Объектно-ориентированный, с удобным литеральным представлением объекта и т. д., выразительность данных является первоклассной.
- Функции являются гражданами первого класса и могут иметь некоторые общепрограммные приложения.
- Используйте наследование на основе прототипов и расширяйте примитивные типы, такие как Number.
- Благодаря новым функциям, таким как Proxy и Reflect, он обладает сильнымВозможности метапрограммирования.
Возможности богемного языка позволяют поддерживать почти любой внутренний стиль сборки DSL, а невероятно активное сообщество заложило естественную основу для разработчиков.
Естественный недостаток JavaScript заключается в том, что его синтаксис, производный от C, приводит к большому количеству шума, и часть этого недостатка можно устранить, используя некоторые варианты языков, такие как CoffeeScript.
Библиотека (интерфейс) или внутренний DSL
Граничной проблемой внешнего DSL часто является различие между DSL и GPPL, что не вызывает особых споров в сообществе. А дискуссия о внутреннем DSL, особенно о разнице с библиотекой (интерфейсом), никогда не прекращалась, и там действительно есть неясные моменты.
На самом деле у DSL есть и другое названиегладкий интерфейс, так что это также режим инкапсуляции интерфейса или инкапсуляции библиотек, и цель состоит в том, чтобы ограничить выразительность. Однако по сравнению с традиционной инкапсуляцией интерфейса он имеет несколько существенных конструктивных отличий:
- лингвистический.
- Не связан традиционными передовыми методами программирования, такими как разделение команд и запросов, закон Деметры и т. д.
Например, во внутреннем DSL получите код видаfoo.should.be.a.number
Это похоже на целое предложение, связанное с заданной грамматикой, а не на набор императивного кода. пока в jQueryhtml
То есть метод запроса (.html()
) также является командным методом (.html('content to set')
), что явно отклоняется от принципа разделения команд и запросов. Основная цель их дизайна - «чрезвычайно плавная выразительность», а не традиционные принципы абстракции инкапсуляции, такие как четкие обязанности и уменьшенная связанность.
На самом деле эта статья больше согласуется с г-ном Мацумото Юкихиро в«Будущее кода»Приведенная в , точка зрения, которая окончательно разрешила сомнения и узлы автора по поводу внутреннего DSL:
дизайн библиотеки — это дизайн языка
Язык программирования определяет только базовую грамматическую основу и небольшое количество словарного запаса, а дизайн библиотеки должен сочетать его с классами, методами, свойствами и даже переменными, которые действуют как словарные запасы, и органично объединять их семантически., и, наконец, реализовать цель дизайна «при ограниченной задаче программистам нужно обращать внимание только на то, что, а не на то, как». То есть2.weeks.ago
Магия программирования (языка) должна быть такой, чтобы достичь более высокого уровня абстракции.
Так что вместо того, чтобы пытаться установить четкую границу для внутреннего DSL, улучшите дизайн своего интерфейса в соответствии с его требованиями. Вот еще один, более радикальный момент:
Programming is a process of designing DSL for your own application.
Некоторые подводные камни внутренней практики DSL
В дополнение к отсутствующим функциональным возможностям и дополнительному синтаксическому шуму из-за зависимости от основного языка у внутренних DSL есть и другие проблемы, которые нельзя игнорировать.
недружелюбное исключение
существует[Стиль 3: Каскадные свойства]В этом случае мы фактически не определяемminutes
Этот блок при неправильном использовании(5).minutes.later
, вы получите следующее сообщение об ошибке:
Uncaught TypeError: Cannot read property 'later' of undefined
Вместо аналогичного сообщения об ошибке мы ожидали:
Uncaught SyntaxError: Unexpected unit minutes
Это связано с тем, что механизм обработки исключений также следует основному языку, и абстракция DSL на уровне инкапсуляции библиотеки по-прежнему не может обойти это ограничение, что также является преимуществом внешнего DSL, но основанного на[Стиль 6: Динамические прокси]С упомянутым прокси мы все еще можем сделать некоторые тривиальные оптимизации:
const UNITS = ['days','weeks','hours'];
const five = new Proxy(new Number(5), {
get (target, property) {
if(UNITS.indexOf(property) === -1){
throw TypeError(`Invalid unit [${property}] after ${target}`)
}else{
// blablabla
}
}
})
Вставьте в консоль и введитеfive.minutes
, вы увидите более понятное сообщение об ошибке:
Uncaught TypeError: invalid units [minutes] after 5
Легко не заметить дизайн под айсбергом
Точка проектирования внутреннего DSL заключается в том, является ли уровень представления гладким, а отсутствие абстрактных требований к инкапсуляции для базовой модели предметной области может привести к отсутствию эффективного проектирования «ядра» DSL. Практикуя DSL, мы по-прежнему должны следовать передовым методам программирования на уровне модели предметной области, таким как эта статья.2.weeks.later
ПозадиDuration
и другие объекты модели предметной области.
Автор когда-то расширил плавный интерфейс API, похожий на jQuery, для крупного внутреннего фреймворка с длинной историей (не распыляйтесь в 2012 году), и это стоило всего меньше, чем500 строк кода, это в основном связано с глубокими навыками проектирования и согласованностью в нижней части фреймворка, а не с моей синтаксической сахарной упаковкой верхнего уровня DSL.
Кроме того, при проектировании DSL одинаково важны синтаксис и семантика.Многие из приведенных выше примеров также доказывают, что простота синтаксиса не обязательно обеспечивает беглость, и он должен быть разработан в сочетании с семантической моделью.
О синтаксисе и семантике:
a || b
а такжеa or b
Синтаксис другой, но семантика та же;a > b
(Ява) иa > b
(Шелл) синтаксис тот же, но семантика разная
Эта часть совета не менее важна при проектировании внешних DSL.
Поддержка редактора
Некоторые внутренние DSL полагаются на набор текста для достижения наилучшей производительности, и большинство языков (включая внешние DSL) имеют механизмы автоматического форматирования, основанные на анализе синтаксического дерева, но внутренним DSL не так повезло, потому что они находятся на фактическом уровне синтаксиса. Определение отсутствует. , поэтому часто случается, что редактор использует «Форматировать документ» для сбоя, что менее распространено в языках, основанных на отступах.
Специальная подсветка кода еще сложнее, и даже для поддержки автодополнения требуется дополнительная работа.
резюме
В обычном программном решении выражение больше «как», то есть детали того, как его достичь.Участвующие элементы программирования, такие как выражения, операторы и структуры данных, будут влиять на понимание исходной проблемы по предметной области. рабочие. Секрет DSL заключается в том, что он подчеркивает выражение «Что», преобразовывая исходное императивное программирование в конечное декларативное выражение, делая DSL мощным и не требующим пояснений, тем самым повышая эффективность программирования и даже расширяя возможности пользователей, не имеющих опыта программирования.
В этой статье в основном объясняется практика внутреннего DSL, важной ветви интерфейса, и описываются 8 стилей реализации в сочетании с некоторыми типичными примерами в области Javascript и интерфейса, и подчеркивается, что эти стили не являются независимыми «серебряными пулями». , но взаимное пополнение.
В этой статье также обсуждаются некоторые мифы.Мы исследуем возможность использования Javascript в качестве внутреннего хост-языка DSL и подчеркиваем идею о том, что «рекомендации по проектированию DSL должны быть более важными, чем определения его границ», и, наконец, приведем к некоторым внутренним DSL. Распространенные ошибки в процессе проектирования DSL.
дальнейшее чтение
Следите за новостями во второй части этой статьи — Внешние DSL, и следующие книги помогут вам в дальнейшем обучении:
- Языки домена - Martin Fowler
- «Практика предметно-ориентированного языка» - Debasish Ghosh
Релевантная информация
- «Будущее кода»- Мацумото Юкихиро
- Рождение Javascript
- Declarative programming is it a real thing?
- Расскажите о DSL и применении DSL (в качестве примера возьмем CocoaPods).
- Построить внутреннюю DSL с плавным интерфейсом
Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!