Декларативное программирование: реально ли это?

Опубликовано: 2022-03-11

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

В двух словах, декларативное программирование состоит в том, чтобы указать программе, что нужно сделать, вместо того, чтобы сказать ей, как это сделать. На практике этот подход влечет за собой предоставление предметно-ориентированного языка (DSL) для выражения того , что хочет пользователь, и ограждение его от низкоуровневых конструкций (циклов, условий, присваиваний), которые материализуют желаемое конечное состояние.

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

ПРЕДОСТЕРЕЖЕНИЕ : Эта статья появилась в результате многолетней личной борьбы с декларативными инструментами. Многие утверждения, которые я здесь представляю, не полностью доказаны, а некоторые даже представлены за чистую монету. Надлежащая критика декларативного программирования потребовала бы значительного времени и усилий, и мне пришлось бы вернуться и использовать многие из этих инструментов; мое сердце не в таком начинании. Цель этой статьи — поделиться с вами несколькими мыслями, без лишних слов и показать, что сработало для меня. Если вы боролись с инструментами декларативного программирования, вы можете найти передышку и альтернативы. И если вам нравится эта парадигма и ее инструменты, не принимайте меня слишком серьезно.

Если декларативное программирование работает для вас хорошо, я не могу сказать вам обратное .

Вы можете любить или ненавидеть декларативное программирование, но вы не можете позволить себе игнорировать его.
Твитнуть

Достоинства декларативного программирования

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

Возможно, наиболее успешным инструментом декларативного программирования является реляционная база данных (RDB). Возможно, это даже первый декларативный инструмент. В любом случае РБД обладают двумя свойствами, которые я считаю типичными для декларативного программирования:

  • Язык предметной области (DSL) : универсальный интерфейс для реляционных баз данных — это DSL, называемый языком структурированных запросов, наиболее известный как SQL.
  • DSL скрывает уровень нижнего уровня от пользователя : начиная с оригинальной статьи Эдгара Ф. Кодда о RDB ясно, что сила этой модели заключается в отделении желаемых запросов от базовых циклов, индексов и путей доступа, которые их реализуют.

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

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

Каковы обычно перечисляемые преимущества декларативного программирования?

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

Сторонники декларативного программирования быстро указывают на его преимущества. Однако даже они признают, что приходится идти на компромиссы.
Твитнуть
  1. Читабельность/удобство использования : DSL обычно ближе к естественному языку (например, английскому), чем к псевдокоду, следовательно, более удобочитаем, а также легче изучается непрограммистами.
  2. Краткость : большая часть шаблона абстрагируется DSL, оставляя меньше строк для выполнения той же работы.
  3. Повторное использование : проще создавать код, который можно использовать для разных целей; то, что, как известно, сложно при использовании императивных конструкций.
  4. Идемпотентность : вы можете работать с конечными состояниями и позволить программе понять это за вас. Например, с помощью операции upsert вы можете либо вставить строку, если ее там нет, либо изменить ее, если она уже есть, вместо того, чтобы писать код для обработки обоих случаев.
  5. Исправление ошибок : легко указать конструкцию, которая будет останавливаться при первой ошибке, вместо того, чтобы добавлять прослушиватели ошибок для каждой возможной ошибки. (Если вы когда-либо писали три вложенных обратных вызова в node.js, вы понимаете, что я имею в виду.)
  6. Ссылочная прозрачность : хотя это преимущество обычно ассоциируется с функциональным программированием, на самом деле оно действительно для любого подхода, который сводит к минимуму ручную обработку состояния и опирается на побочные эффекты.
  7. Коммутативность : возможность выражения конечного состояния без указания фактического порядка, в котором оно будет реализовано.

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

  1. Слой высокого уровня, адаптированный к конкретной предметной области : декларативное программирование создает слой высокого уровня, используя информацию предметной области, к которой он применяется. Понятно, что если мы имеем дело с базами данных, нам нужен набор операций для работы с данными. Большинство из семи вышеперечисленных преимуществ связаны с созданием высокоуровневого слоя, точно адаптированного к конкретной предметной области.
  2. Poka-yoke (защита от дурака) : высокоуровневый уровень, ориентированный на предметную область, скрывает императивные детали реализации. Это означает, что вы совершаете гораздо меньше ошибок, потому что низкоуровневые детали системы просто недоступны. Это ограничение устраняет многие классы ошибок из вашего кода.

Две проблемы с декларативным программированием

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

Проблема с DSL: обособленность

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

Самое простое решение — генерировать HTML путем конкатенации строк — кажется настолько ужасным, что вам придется быстро искать альтернативу. Стандартным решением является использование системы шаблонов. Хотя существуют различные типы систем шаблонов, мы не будем рассматривать их различия в целях данного анализа. Мы можем считать, что все они похожи в том, что основная задача систем шаблонов — предоставить альтернативу коду, который объединяет строки HTML с использованием условий и циклов, подобно тому, как RDB появились как альтернатива коду, который зацикливался на записях данных.

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

Я делаю исключение для встроенных шаблонов Ruby (ERB), потому что они интегрированы в исходный код Ruby. Это не относится к инструментам, вдохновленным ERB, написанным на других языках, поскольку эти шаблоны также должны храниться в виде разных файлов.

Второй источник трений заключается в том, что DSL имеет собственный синтаксис, отличный от синтаксиса вашего языка программирования. Следовательно, изменить DSL (не говоря уже о написании собственного) значительно сложнее. Чтобы зайти под капот и изменить инструмент, вам нужно узнать о токенизации и парсинге, что интересно и сложно, но сложно. Я считаю это недостатком.

Вы можете спросить: «С какой стати вам нужно модифицировать свой инструмент? Если вы делаете стандартный проект, вам подойдет хорошо написанный стандартный инструмент». Может быть да, может быть нет.

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

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

  • Замена текста в шаблоне : подстановка переменных.
  • Повторение шаблона : петли.
  • Избегайте печати шаблона, если условие не выполняется : условия.
  • Частицы : подпрограммы.
  • Помощники : подпрограммы (единственное отличие от частичных состоит в том, что помощники могут получить доступ к базовому языку программирования и освободить вас от смирительной рубашки DSL).

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

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

Раздельность — неотъемлемая проблема любой DSL, независимо от того, насколько хорошо она спроектирована.

Теперь обратимся ко второй проблеме декларативных инструментов, которая широко распространена, но не является неотъемлемой.

Еще одна проблема: отсутствие развертывания приводит к сложности

Если бы я написал эту статью несколько месяцев назад, этот раздел был бы назван Большинство декларативных инструментов #@!$#@! Сложный, но я не знаю почему . В процессе написания этой статьи я нашел лучший способ выразить ее: большинство декларативных инструментов намного сложнее, чем они должны быть . Оставшуюся часть этого раздела я потрачу на объяснение почему. Для анализа сложности инструмента я предлагаю меру, называемую разрывом сложности . Разрыв в сложности — это разница между решением данной проблемы с помощью инструмента и ее решением на более низком уровне (предположительно, в простом императивном коде), который инструмент намеревается заменить. Когда первое решение сложнее второго, мы имеем дело с разрывом сложности. Под более сложным я подразумеваю большее количество строк кода, код, который труднее читать, сложнее модифицировать и сложнее поддерживать, но не обязательно все это одновременно.

Обратите внимание, что мы сравниваем решение более низкого уровня не с лучшим инструментом, а с отсутствием инструмента. Это перекликается с медицинским принципом «Во-первых, не навреди» .

Признаками инструмента с большим разрывом сложности являются:

  • Что-то, что требует нескольких минут для подробного описания в императивных терминах, потребует часов для написания кода с использованием инструмента, даже если вы знаете, как использовать этот инструмент.
  • Вы чувствуете, что постоянно работаете вокруг инструмента, а не с ним.
  • Вы изо всех сил пытаетесь решить простую проблему, которая полностью относится к области используемого вами инструмента, но лучший ответ на вопрос о переполнении стека, который вы найдете, описывает обходной путь .
  • Когда эта очень простая проблема может быть решена с помощью определенной функции (которой нет в инструменте), и вы видите проблему Github в библиотеке, в которой есть длинное обсуждение указанной функции с вкраплениями +1 .
  • Хроническое, зудящее желание бросить инструмент и сделать все самому внутри _цикла for_.

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

При этом не обязательно неприемлемо, чтобы инструмент был несколько более сложным, чем нижний уровень, который он намеревается заменить; если инструмент дает код, который является более читаемым, кратким и правильным, это может стоить того. Это проблема, когда инструмент в несколько раз сложнее, чем проблема, которую он заменяет; это категорически неприемлемо. Брайан Керниган однажды сказал: « Управление сложностью — суть компьютерного программирования. «Если инструмент значительно усложняет ваш проект, зачем вообще его использовать?

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

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

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

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

Чтобы подчеркнуть, почему это прекрасные примеры развертывания, я хотел бы процитировать несколько выдержек из статьи Денниса Ритчи, одного из авторов Unix, 1979 года:

При пакетных заданиях :

… новая схема управления технологическим процессом сразу сделала реализацию некоторых очень ценных функций тривиальной; например, отдельные процессы (с & ) и рекурсивное использование оболочки в качестве команды. Большинство систем должны предоставлять какое-то специальное средство batch job submission и специальный интерпретатор команд для файлов, отличный от того, который используется в интерактивном режиме.

На сопрограммах :

Гениальность конвейера Unix именно в том, что он построен из одних и тех же команд, постоянно используемых в симплексной форме.

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

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

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

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

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

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

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

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

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

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

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

Двойной подход

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

  • Используйте предметно-ориентированный язык структуры данных (dsDSL), чтобы преодолеть разделение.
  • Создайте высокий уровень, который разворачивается из нижнего уровня, чтобы преодолеть разрыв в сложности.

dsDSL

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

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

Если бы вы спросили меня год назад, я бы подумал, что концепция dsDSL была новой, но однажды я понял, что JSON сам по себе является прекрасным примером такого подхода! Анализируемый объект JSON состоит из структур данных, которые декларативно представляют записи данных, чтобы получить преимущества DSL, а также упростить анализ и обработку из языка программирования. (Могут быть и другие dsDSL, но пока я ни с кем не сталкивался. Если вы знаете о таком, я был бы очень признателен, если бы вы упомянули его в разделе комментариев.)

Как и JSON, dsDSL имеет следующие атрибуты:

  1. Он состоит из очень небольшого набора функций: JSON имеет две основные функции, parse и stringify .
  2. Его функции чаще всего получают сложные и рекурсивные аргументы: проанализированный JSON представляет собой массив или объект, который обычно содержит внутри дополнительные массивы и объекты.
  3. Входные данные для этих функций соответствуют очень специфическим формам: JSON имеет явную и строго соблюдаемую схему проверки, позволяющую отличить допустимые структуры от недопустимых.
  4. И входы, и выходы этих функций могут содержаться и генерироваться языком программирования без отдельного синтаксиса.

Но dsDSL во многом превосходят JSON. Давайте создадим dsDSL для генерации HTML с помощью Javascript. Позже я коснусь вопроса о том, можно ли этот подход распространить на другие языки (спойлер: это точно можно сделать в Ruby и Python, но вряд ли в C).

HTML — это язык разметки, состоящий из tags , разделенных угловыми скобками ( < и > ). Эти теги могут иметь необязательные атрибуты и содержимое. Атрибуты — это просто список атрибутов ключ/значение, а содержимое может быть либо текстом, либо другими тегами. И атрибуты, и содержимое являются необязательными для любого данного тега. Я несколько упрощаю, но это точно.

Простой способ представить HTML-тег в dsDSL — использовать массив из трех элементов: — Тег: строка. - Атрибуты: объект (простой, типа ключ/значение) или undefined (если атрибуты не нужны). - Содержимое: строка (текст), массив (другой тег) или undefined (если содержимого нет).

Например, <a href="views">Index</a> можно записать как ['a', {href: 'views'}, 'Index'] .

Если мы хотим встроить этот элемент привязки в div со links на классы, мы можем написать: ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']] .

Чтобы перечислить несколько тегов html на одном уровне, мы можем обернуть их в массив:

 [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]

Тот же принцип можно применить к созданию нескольких тегов внутри тега:

 ['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]

Конечно, на этом dsDSL далеко не уедешь, если мы не будем генерировать из него HTML. Нам нужна функция generate , которая возьмет наш dsDSL и выдаст строку с HTML. Итак, если мы запустим generate (['a', {href: 'views'}, 'Index']) , мы получим строку <a href="views">Index</a> .

Идея любого DSL состоит в том, чтобы указать несколько конструкций с определенной структурой, которые затем передаются функции. В этом случае структурой, составляющей dsDSL, является этот массив, состоящий из одного-трех элементов; эти массивы имеют определенную структуру. Если generate тщательно проверяет свои входные данные (а тщательно проверить входные данные и легко, и важно, поскольку эти правила проверки являются точным аналогом синтаксиса DSL), он точно скажет вам, где вы ошиблись при вводе. Через некоторое время вы начнете понимать, что отличает действующую структуру в dsDSL, и эта структура будет сильно напоминать лежащую в ее основе вещь, которую она генерирует.

Итак, каковы преимущества dsDSL по сравнению с DSL?

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

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

 var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]

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

Более того, у нас есть переменная FILTER , которая при инициализации будет массивом категорий, которые мы хотим отобразить.

Мы хотим, чтобы наша таблица:

  • Отображение заголовков таблицы.
  • Для каждого товара показать поля: описание, цена и категории.
  • Не печатайте поле id , а добавляйте его в качестве атрибута id для каждой строки. АЛЬТЕРНАТИВНАЯ ВЕРСИЯ: добавьте атрибут id к каждому элементу tr .
  • Разместите класс onSale , если товар продается со скидкой.
  • Отсортируйте товары по убыванию цены.
  • Отфильтруйте определенные продукты по категориям. Если FILTER — пустой массив, мы отобразим все товары. В противном случае мы будем отображать только те продукты, категория которых содержится в FILTER .

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

 function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }

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

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

 var drawTable = function (DATA, FILTER) {

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

 var printableFields = ['description', 'price', 'categories'];

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

 DATA.sort (function (a, b) {return a.price - b.price});

Здесь мы возвращаем литерал объекта; массив, который содержит table в качестве первого элемента и ее содержимое в качестве второго. Это dsDSL-представление <table> , которое мы хотим создать.

 return ['table', [

Теперь мы создаем строку с заголовками таблицы. Чтобы создать его содержимое, мы используем dale.do, функцию, аналогичную Array.map, но также работающую с объектами. Мы будем перебирать printableFields и генерировать заголовки таблиц для каждого из них:

 ['tr', dale.do (printableFields, function (field) { return ['th', field]; })],

Обратите внимание, что мы только что реализовали итерацию, рабочую лошадку генерации HTML, и нам не понадобились какие-либо конструкции DSL; нам нужна была только функция для итерации структуры данных и возврата dsDSL. Аналогичная нативная или реализованная пользователем функция также сработала бы.

Теперь выполните итерацию по продуктам, содержащимся в DATA .

 dale.do (DATA, function (product) {

Мы проверяем, не остался ли этот товар в FILTER . Если FILTER пуст, мы напечатаем продукт. Если FILTER не пуст, мы будем перебирать категории продукта, пока не найдем ту, которая содержится в FILTER . Мы делаем это с помощью dale.stop.

 var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });

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

Если matches равно false , мы возвращаем пустой массив (поэтому мы не печатаем этот продукт). В противном случае мы возвращаем <tr> с его правильным идентификатором и классом, и мы перебираем printableFields , чтобы напечатать поля.

 return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];

Конечно, мы закрываем все, что мы открыли. Разве синтаксис не забавен?

 })]; }) ]]; }

Теперь, как нам включить эту таблицу в более широкий контекст? Мы пишем функцию с именем drawAll , которая будет вызывать все функции, генерирующие представления. Помимо drawTable , у нас также могут быть drawHeader , drawFooter и другие аналогичные функции, каждая из которых будет возвращать dsDSL .

 var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }

Если вам не нравится, как выглядит приведенный выше код, ничто из того, что я скажу, вас не убедит. Это dsDSL в лучшем виде . You might as well stop reading the article (and drop a mean comment too because you've earned the right to do so if you've made it this far!). But seriously, if the code above doesn't strike you as elegant, nothing else in this article will.

For those who are still with me, I would like to go back to the main claim of this section, which is that a dsDSL has the advantages of both the high and the low level :

  • The advantage of the low level resides in writing code whenever we want, getting out of the straightjacket of the DSL.
  • The advantage of the high level resides in using literals that represent what we want to declare and letting the functions of the tool convert that into the desired end state (in this case, a string with HTML).

But how is this truly different from purely imperative code? I think ultimately the elegance of the dsDSL approach boils down to the fact that code written in this way mostly consists of expressions, instead of statements. More precisely, code that uses a dsDSL is almost entirely composed of:

  • Literals that map to lower level structures.
  • Function invocations or lambdas within those literal structures that return structures of the same kind.

Code that consists mostly of expressions and which encapsulate most statements within functions is extremely succinct because all patterns of repetition can be easily abstracted. You can write arbitrary code as long as that code returns a literal that conforms to a very specific, non-arbitrary form.

A further characteristic of dsDSLs (which we don't have time to explore here) is the possibility of using types to increase the richness and succinctness of the literal structures. I will expound on this issue on a future article.

Might it be possible to create dsDSLs beyond Javascript, the One True Language? I think that it is, indeed, possible, as long as the language supports:

  • Literals for: arrays, objects (associative arrays), function invocations, and lambdas.
  • Runtime type detection
  • Polymorphism and dynamic return types

I think this means that dsDSLs are tenable in any modern dynamic language (ie: Ruby, Python, Perl, PHP), but probably not in C or Java.

Walk, Then Slide: How To Unfold The High From The Low

In this section I will attempt to show a way for unfolding a high level tool from its domain. In a nutshell, the approach consists of the following steps

  1. Take two to four problems that are representative instances of a problem domain. These problems should be real. Unfolding the high level from the low one is a problem of induction, so you need real data to come up with representative solutions.
  2. Solve the problems with no tool in the most straightforward way possible.
  3. Stand back, take a good look at your solutions, and notice the common patterns among them.
  4. Find the patterns of representation (high level).
  5. Find the patterns of generation (low level).
  6. Solve the same problems with your high level layer and verify that the solutions are indeed correct.
  7. If you feel that you can easily represent all the problems with your patterns of representation, and the generation patterns for each of these instances produce correct implementations, you're done. Otherwise, go back to the drawing board.
  8. If new problems appear, solve them with the tool and modify it accordingly.
  9. The tool should converge asymptotically to a finished state, no matter how many problems it solves. In other words, the complexity of the tool should remain constant, rather than growing with the amount of problems it solves.

Now, what the hell are patterns of representation and patterns of generation ? I'm glad you asked. The patterns of representation are the patterns in which you should be able to express a problem that belongs to the domain that concerns your tool. It is an alphabet of structures that allows you to write any pattern you might wish to express within its domain of applicability. In a DSL, these would be the production rules. Let's go back to our dsDSL for generating HTML.

Breaking down an HTML snippet. The line

The humble HTML tag is a good example of patterns of representation. Let's take a closer look at these basic patterns.
Твитнуть

The patterns of representation for HTML are the following:

  • A single tag: ['TAG']
  • A single tag with attributes: ['TAG', {attribute1: value1, attribute2: value2, ...}]
  • A single tag with contents: ['TAG', 'CONTENTS']
  • A single tag with both attributes and contents: ['TAG', {attribute1: value1, ...}, 'CONTENTS']
  • A single tag with another tag inside: ['TAG1', ['TAG2', ...]]
  • A group of tags (standalone or inside another tag): [['TAG1', ...], ['TAG2', ...]]
  • Depending on a condition, place a tag or no tag: condition ? ['TAG', ...] : [] / Depending on a condition, place an attribute or no attribute: ['TAG', {class: condition ? 'someClass': undefined}, ...]

These instances can be represented with the dsDSL notation we determined in the previous section. And this is all you need to represent any HTML you might need. More sophisticated patterns, such as conditional iteration through an object to generate a table, may be implemented with functions that return the patterns of representation above, and these patterns map directly to HTML tags.

If the patterns of representation are the structures you use to express what you want, the patterns of generation are the structures your tool will use to convert patterns of representation into the lower level structures. For HTML, these are the following:

  • Validate the input (this is actually is an universal pattern of generation).
  • Open and close tags (but not the void tags, like <input> , which are self-closing).
  • Place attributes and contents, escaping special characters (but not the contents of the <style> and <script> tags).

Believe it or not, these are the patterns you need to create an unfolding dsDSL layer that generates HTML. Similar patterns can be found for generating CSS. In fact, lith does both, in ~250 lines of code.

One last question remains to be answered: What do I mean by walk, then slide ? When we deal with a problem domain, we want to use a tool that delivers us from the nasty details of that domain. In other words, we want to sweep the low level under the rug, the faster the better. The walk, then slide approach proposes exactly the opposite: spend some time on the low level. Embrace its quirks, and understand which are essential and which can be avoided in the face of a set of real, varied, and useful problems.

After walking in the low level for some time and solving useful problems, you will have a sufficiently deep understanding of their domain. The patterns of representation and generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. You can then write code that employs them. If they work, you will be able to slide through problems where you recently had to walk through them. Sliding means many things; it implies speed, precision and lack of friction. Maybe more importantly, this quality can be felt; when solving problems with this tool, do you feel like you're walking through the problem, or do you feel that you're sliding through it?

Maybe the most important thing about an unfolded tool is not the fact that it frees us from having to deal with the low level. Rather, by capturing the empiric patterns of repetition in the low level, a good high level tool allows us to understand fully the domain of applicability.

An unfolded tool will not just solve a problem - it will enlighten you about the problem's structure.

So, don't run away from a worthy problem. First walk around it, then slide through it.

Related: Introduction To Concurrent Programming: A Beginner's Guide