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

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

Первое отличие в моем подходе мне давно известно. Я пытаюсь преодолеть недальновидность, создавая общий код, где это возможно. Вероятно, из-за моей врожденной склонности уменьшать масштаб и видеть картину в целом, написание несколько более общих решений обычно не требует дополнительных усилий. Чтобы проиллюстрировать это, обычно реализовать X+Y так же просто, как реализовать 3+4, и, по моему опыту, дальнейшие требования редко останавливаются на достигнутом. Следующим неизбежно идет 2+5, затем 7+4 и так далее.

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

О вложениях

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

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

DSL могут сильно различаться: от полноценных встроенных языков до языков, написанных на синтаксисе основного языка, или даже языков API какой-либо сторонней библиотеки, которая реализует части предметной области [1].

Виды вложений

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

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

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

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

это палитра

Довольно легко провести ассоциацию с десятым правилом Гринспана:

Любая достаточно сложная программа на C или Fortran содержит нерегламентированную, неформально заданную, наполненную ошибками, медленную реализацию половины Common Lisp.

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

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

В некотором смысле палитра вложений — это еще один пример того, что известно как Фундаментальная теорема программной инженерии:[4]

Мы можем решить любую проблему, введя дополнительный уровень косвенности.

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

Как типирование связано с этим?

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

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

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

Заключение

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

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

Рекомендации

  1. Джугель, У. (2010). ван ден Бранд, М., Гашевич, Д., Грей, Дж., ред. Создание библиотек Smart Wrapper для произвольных API. Разработка языка программного обеспечения. SLE 2009. Конспект лекций по информатике. Шпрингер, Берлин, Гейдельберг. 5969. DOI: 10.1007/978–3–642–12107–4_24
  2. Жером Вуйон. Неглубокое вложение логики в Coq. Университет Париж Дидро — Париж 7, CNRS https://www.cis.upenn.edu/~sweirich/wmm/wmm08/vouillon.pdf
  3. https://en.wikipedia.org/wiki/Expression_problem
  4. https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering