Разработчик веб-приложений и распределённых систем под псевдонимом chreke* убеждён: «малые языки», то есть специализированные языки, созданные для решения конкретных задач, являются будущим программирования. Это убеждение укрепилось в нём после прочтения статьи Габриэллы Гонсалес «Конец истории программирования» и просмотра лекции Алана Кея «Программирование и масштабирование».
Под катом автор объясняет, что подразумевает под «малыми языками», и почему они так важны.
*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.
Что представляет собой «малый язык»?
Полагаю, что термин «малый язык» был впервые использован Джоном Бентли в статье под названием «Малые языки», где он дал ему следующее определение:
[...] малый язык ориентирован на конкретную проблемную область и не содержит многие функции, которые есть в обычных языках.
Например, SQL — это язык, предназначенный для описания операций с базами данных. Регулярные выражения — это язык для поиска и замены текста. Dhall — язык для управления конфигурациями и т. д.
У этих языков есть и другие названия: предметно-ориентированные языки (DSL), проблемно-ориентированные языки и т. д. Однако мне больше всего нравится термин «малые языки». Во-первых, потому что термин DSL стал слишком обобщенным и может относиться как к библиотекам с понятным интерфейсом, так и к полноценному языку запросов, например, SQL. Во-вторых, потому что термин «малые языки» подчеркивает их компактность.
Зачем нужны малые языки?
Современное ПО, с точки зрения истории, — инженерия, но это та инженерия, которую осуществляли люди, не знакомые с концепцией арки. Большинство современного программного обеспечения напоминает египетскую пирамиду: миллионы кирпичей, сложенных друг на друга без структурной целостности, созданные лишь благодаря грубой силе и труду тысяч рабов.
В сообществе разработчиков ПО существует серьёзная проблема — чем сложнее становится приложение, тем более объёмным будет его исходный код. Но наша способность понимать большие объёмы кода остается практически неизменной. По данным исследования The Emergence of Big Code («Возникновение большого кода»), проведенного Sourcegraph в 2020 году, большинство респондентов отметили, что размер их кодовой базы приводит к одной или нескольким из следующих проблем:
-
Трудности с адаптацией новых сотрудников
-
Сбои в коде из-за недостаточного понимания зависимостей
-
Управление изменениями в коде становится все сложнее
Более того, приложения, похоже, растут с пугающей скоростью: большинство участников опроса Sourcegraph оценили, что их кодовая база увеличилась в 100-500 раз за последние десять лет. В качестве наглядного примера можно привести ядро Linux, которое в 1992 году состояло из примерно 10 000 строк кода. Спустя двадцать лет, его размер См. <a href="https://www.phoronix.com/news/Linux-Git-Stats-EOY2019">https://www.phoronix.com/news/Linux-Git-Stats-EOY2019</a></p>" data-abbr="достиг около 30 миллионов строк">достиг около 30 миллионов строк.
Откуда берется столько кода? Я не думаю, что расширение функциональности может объяснить такой рост его объёма; скорее, это связано с нашим подходом к разработке ПО. Обычно новые функции в программе добавляются поверх уже существующих, как при строительстве пирамиды. Проблема в том, что, как и в случае с пирамидой, каждый новый слой требует больше «кирпичей», чем предыдущий.
Противостояние тенденции
Действительно ли для создания современной операционной системы требуются миллионы строк кода? В 2006 году Алан Кей вместе со своими коллегами из программы STEPS решили опровергнуть следующее предположение:
Наука развивается через взаимосвязь эмпирических исследований и теоретических моделей, поэтому наш первый вопрос как учёных звучит так: если мы создадим работающую модель феномена персональных вычислений, сможет ли она быть упрощена до уровня уравнений Максвелла для всего электромагнитного спектра, или Конституции США, которую можно носить в кармане рубашки, или же она настолько беспорядочна (или действительно сложна), что потребует «3 кубических мили прецедентного права», как в американской правовой системе (или, возможно, в современной практике программного обеспечения)? Ответ, скорее всего, где-то посередине, и было бы очень интересно, если бы он оказался ближе к простому концу, а не к другой огромной хаотичной крайности.
Так что мы задаёмся вопросом: является ли опыт персональных вычислений (учитывая эквивалент операционной системы, приложений и другого вспомогательного программного обеспечения) по своей сути 2 миллиардами строк кода, 200 миллионами, 20 миллионами, 2 миллионами, 200 000, 20 000, 2 000?
— Отчёт о проделанной работе в рамках проекта STEPS, 2007 г., стр. 4–5
Уравнения Максвелла, которые упоминает доктор Кей, — это набор уравнений, описывающих основы электромагнетизма, оптики и электрических цепей. Интересной особенностью этих уравнений является то, что несмотря на их широкий диапазон применения, они настолько компактны, что могут поместиться даже на футболке:
Одной из причин их лаконичности является использование оператора набла (например,∇ ) для описания операций векторного исчисления. Важно понимать, что набла — это не совсем оператор, это скорее сокращение, которое облегчает работу с некоторыми уравнениями в векторном исчислении.
А что если возможно создать аналог оператора набла для программирования? Так же как оператор набла помогает упростить векторное исчисление, не могут ли существовать обозначения, которые помогли бы нам аналогичным образом работать с программами? Этот вопрос был одной из «главных идей», которые стали основой для проекта STEPS:
Мы также уверены, что создание языков, подходящих для решения конкретных задач, упрощает процесс, делает решения более понятными и компактными, что полностью соответствует нашему «активно-математическому» подходу. Эти «проблемно-ориентированные языки» будут создаваться и применяться для решения как крупных, так и мелких задач на различных уровнях абстракции и детализации.
— Отчёт о проделанной работе в рамках проекта STEPS, 2007 г., стр. 6
Суть идеи заключается в том, что когда вы начинаете находить закономерности в своем приложении, вы можете закодировать их при помощи малого языка. Этот язык позволит вам выразить эти закономерности более компактно, чем это возможно с помощью других средств абстракции. Что позволит не только противостоять тенденции к постоянному увеличению размера приложений, но и сократить объём кода в процессе разработки!
Одним из результатов проекта STEPS, который меня особенно впечатлил, стал Nile — малый язык для описания процессов рендеринга и композитинга графики. Целью было использовать Nile для достижения функционального равенства с Cairo — рендерером с открытым исходным кодом, используемым в различных проектах свободного программного обеспечения, общий объём кода которого составляет около 44 000 строк. Аналог на языке Nile в итоге <a href="http://www.vpri.org/pdf/tr2009016_steps09.pdf"><strong>Отчет о проделанной работе в рамках проекта STEPS, 2009 г.</strong></a>, стр. 4–6</p>" data-abbr="составил всего около 300 строк">составил всего около 300 строк.
Почему мы не используем языки высокого уровня?
Но Ada не станет той волшебной серебряной пулей, которая победит монстра производительности программного обеспечения. В конце концов, это всего лишь еще один язык высокого уровня, и наибольшая польза от таких языков была получена при первом переходе от непреднамеренных сложностей к более абстрактному представлению пошаговых решений. После устранения этих непреднамеренных сложностей, оставшиеся становятся меньше, и отдача от их устранения, безусловно, будет меньше.
«Но, подождите-ка, — возможно, скажете вы, — почему бы нам просто не создать язык общего назначения более высокого уровня?» Я лично убежден, что мы достигли предела выразительности языков общего назначения. Если есть уровень выше, то как он будет выглядеть? Возьмем, к примеру, Python — он настолько высокоуровневый, что Цитата Питера Норвига: «Я выбрал Python не потому, что считал его лучше / более приемлемым / более прагматичным чем Lisp, а потому, что это был лучший псевдокод». См. <a href="https://news.ycombinator.com/item?id=1803815"><strong>https://news.ycombinator.com/item?id=1803815</strong></a></p>" data-abbr="практически выглядит как псевдокод">практически выглядит как псевдокод.
Сложность языков общего назначения заключается в том, что вам все же придётся преобразовать вашу задачу в алгоритм и затем представить этот алгоритм на выбранном языке. Языки высокого уровня идеально подходят для описания алгоритмов, но если целью не была реализация алгоритма, то это лишь непреднамеренная сложность.
При написании этого поста мне вспомнилась история о Дональде Кнуте: Кнута попросили продемонстрировать его стиль «грамотного программирования» Опубликована в журнале <a href="https://homepages.cwi.nl/~storm/teaching/reader/BentleyEtAl86.pdf"><strong>Communications of the ACM в июне 1986 года</strong></a></p>" data-abbr="в колонке «Жемчужины программирования»">в колонке «Жемчужины программирования» Джона Бентли; Дуг МакИлрой также был приглашен для критического анализа программы Кнута. Задачей было найти k-ое самое распространённое слово в тексте.
Кнут тщательно написал решение на WEB, его собственной версии языка Pascal для грамотного программирования. Он даже разработал специальную структуру данных для отслеживания количества слов, и все это уложилось в десять страниц кода. МакИлрой высоко оценил мастерство решения Кнута, но сама программа его не особо впечатлила. В рамках своей критики он написал собственное решение на креольском языке, состоящем из shell-скриптов, команд Unix и малых языков:
tr -cs A-Za-z 'n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
Этот код может быть не самым понятным для тех, кто не является экспертами по Unix, и возможно МакИлрой согласился бы с этим, так как решил включить аннотированную версию. Тем не менее этот краткий ответ, вероятно, легче понять, чем десятистраничную программу.
Команды Unix созданы для работы с текстом, именно поэтому позволяют написать такую компактную программу для подсчёта слов. Возможно, shell-скрипт имеет смысл рассматривать как аналог оператора набла для работы с текстом?
Меньше — значит больше
Пример команд Unix выше демонстрирует еще одну характеристику малых языков: менее мощные языки и более высокая производительность. Гонсалес в своей работе «Конец истории программирования» отмечает следующее:
Изучая указанные тенденции, можно увидеть общую закономерность:
-
Перевод пользовательской задачи в задачу рабочей среды, которая:
-
... делает программы более схожими с математическими выражениями, и:
-
... значительно увеличивает сложность рабочей среды.
Регулярные выражения и SQL позволяют вам осуществить только текстовый поиск и операции с базой данных. Их можно противопоставить такому языку как C, где нет рабочей среды и можно выразить всё, что возможно на архитектуре фон Неймана. Высокоуровневые языки, такие как Python и Haskell, занимают промежуточное положение: управление памятью осуществляется автоматически, но у вас всё ещё есть все возможности языка, полного по Тьюрингу, что позволяет выразить любые возможные вычисления.
Малые языки находятся на противоположном конце спектра мощности от C: архитектура компьютера абстрагирована, а также ограничены типы программ, которые вы можете создать — они по своей сути являются неполными по Тьюрингу. Может показаться, что они сильно ограничены, но на самом деле такие языки открывают новые возможности для оптимизации и статического анализа. И, подобно тому, как абстрагирование управления памятью может позволить избежать целого класса ошибок, можно устранить еще больше ошибок, максимально абстрагируясь от алгоритмической работы.
Статический анализ
Языки с меньшей мощностью легче анализировать, и они могут предоставлять более сильные гарантии, чем языки общего назначения. Например, Dhall — это полноценный функциональный язык программирования для создания конфигурационных файлов. Так как вы не хотите рисковать сбоем ваших скриптов развертывания или зацикливанием их, программы на Dhall гарантированно:
-
Не вызовут сбой и
-
Завершатся за конечное время.
Первый пункт достигается путем отказа от выбрасывания исключений; любая операция, которая может не удаться (например, получение первого элемента из потенциально пустого списка), возвращает результат Optional, который может быть со значением или без него. Второй пункт — гарантированное завершение — достигается Здесь можно найти интересную ветку на Hacker News о рекурсии в Dhall: <a href="https://news.ycombinator.com/item?id=15187150"><strong>https://news.ycombinator.com/item?id=15187150</strong></a></p>" data-abbr="путем запрета рекурсивных определений">путем запрета рекурсивных определений. В других функциональных языках программирования основным способом описания циклов является рекурсия, но в Dhall вам придется использовать встроенную функцию fold. Отсутствие универсальной конструкции цикла также означает, что Dhall не является полным по Тьюрингу, но поскольку это не язык программирования общего назначения, ему это и не требуется (Да, похоже, что <a href="https://accodeing.com/blog/2015/css3-proven-to-be-turing-complete"><strong>CSS обладает полнотой по Тьюрингу</strong></a></p>" data-abbr="в отличие, видимо, от CSS">в отличие, видимо, от CSS).
Если языки небольшие, то анализировать их становится ещё проще. Например, определить, не имеет ли произвольная программа на Python побочных эффектов, сложно, но в SQL это легче — просто проверьте, Гарантия становится недействительной, если вы не соблюдаете ISO SQL</p>" data-abbr="начинается ли запрос с SELECT">начинается ли запрос с SELECT
.
Для Nile команда STEPS <a href="http://www.vpri.org/pdf/tr2012001_steps.pdf"><strong>Итоговый отчет STEPS за 2012 год</strong></a>, стр. 12</p><p></p>" data-abbr="увидела необходимость в графическом отладчике">увидела необходимость в графическом отладчике. Брет Виктор (да, тот самый Брет Виктор, который выступал с докладом Inventing on Principle) разработал инструмент для определения точных строк кода, задействованных при рисовании конкретного пикселя на экране. Вы можете посмотреть демонстрацию Алана Кея на YouTube, а также попробовать сделать это самостоятельно. Такие инструменты возможны благодаря тому, что Nile — это небольшой язык, который легко понять. Представьте, что вы пытаетесь сделать то же самое с графическим кодом на C++!
Жажда скорости
Более продвинутые языки программирования не только увеличивают вероятность ошибок, но и могут негативно повлиять на производительность. Например, если программа не формулируется в виде алгоритма, рабочая среда может выбрать свой собственный алгоритм; медленные выражения могут быть заменены на более быстрые, если мы можем доказать, что они дают тот же результат.
Скажем, SQL-запрос не указывает, как он должен выполняться — механизм базы данных может использовать тот план запроса, который он считает наиболее подходящим, например, индекс, комбинацию индексов или просто просканировать всю таблицу базы данных. Современные движки баз данных также собирают статистику о распределении значений в своих столбцах, чтобы они могли выбрать статистически оптимальный план запроса. Это было бы невозможно, если бы запрос был описан с помощью алгоритма.
Одним из секретных ингредиентов, которые позволили языку Nile стать таким компактным, был Jitblt, JIT-компилятор для рендеринга графики. В ходе обсуждений между командами STEPS и Cairo стало ясно, что большая часть кода Cairo посвящена ручной оптимизации операций с пикселями; работа, которую, в теории, можно было бы передать компилятору. Дэн Амеланг из команды Cairo предложил реализовать такой компилятор, и в результате появился Jitblt. Это означало, что работа по оптимизации графического конвейера могла быть отделена от чисто математических описаний того, что нужно отрисовать, что позволило Nile работать <a href="http://www.vpri.org/pdf/tr2007008_steps.pdf"><strong>Отчёт о проделанной работе за первый год проекта STEPS, декабрь 2007 г.</strong></a>, стр. 12</p><p></p>" data-abbr="примерно с такой же скоростью">примерно с такой же скоростью, как и оригинальный, оптимизированный вручную код Cairo.
Малые языки, большие возможности
Что случилось с проектом STEPS? Получился ли у них код, эквивалентный «3 кубическим милям прецедентного права», или им удалось создать операционную систему, которая поместится на футболке? Итогом проекта STEPS стала KSWorld — полная операционная система, включающая редактор документов и редактор таблиц, общий объём кода которой <a href="http://www.vpri.org/pdf/tr2012001_steps.pdf"><strong>Итоговый отчет проекта STEPS, 2012 г.</strong></a>, стр. 32</p>" data-abbr="составил около 17 000 строк">составил около 17 000 строк. Хотя такой код сможет поместиться только на очень большой футболке, я бы все равно назвал этот результат успешным.
Создание KSWorld свидетельствует о том, что в малых языках скрыт огромный потенциал. Однако все еще остается множество неотвеченных вопросов, например, «Как эти малые языки должны взаимодействовать друг с другом?» «Следует ли им объединяться в общее промежуточное представление? Или разные рабочие среды должны работать параллельно и взаимодействовать друг с другом через общий протокол (например, конвейер UNIX или TCP/IP)? Или каждый язык настолько мал, что его можно реализовать заново на различных основных языках (как регулярные выражения)? Возможно, язык будущего — это комбинация всего вышеперечисленного?» В любом случае, я уверен, что нам нужно придумать новый подход к созданию программного обеспечения. Возможно, малые языки станут частью этой истории, а может быть, и нет — главное, чтобы мы смогли придумать что-то получше, вместо того чтобы продолжать складывать кирпичи друг на друга.
Дополнительные материалы
-
Connexion — это API-фреймворк с открытым исходным кодом от Zazzle, который автоматически генерирует конечные точки из спецификации OpenAPI. Обычно OpenAPI используется для описания конечных точек существующего HTTP-сервиса, но Connexion делает всё наоборот — на основе схемы OpenAPI он настраивает сервер API с конечными точками, логикой валидации и живой документацией.
-
Catala — это декларативный язык, предназначенный для преобразования текста закона в исполняемую спецификацию. Так как он поддерживает немонотонную логику (то есть последующее утверждение может отменить или дополнить предыдущее), он позволяет формулировать программы примерно так же, как пишутся юридические тексты, т. е. как набор утверждений, которые можно изменить или дополнить, добавив новые утверждения.
-
Racket — это диалект Lisp, специально разработанный для создания новых языков (этот подход иногда называют языково-ориентированным программированием). Я еще не успел основательно поработать с Racket, но он выглядит весьма подходящим инструментом для создания «малых языков». Если это вас заинтересовало, советую прочитать статью «Создание языков в Racket».
Автор: Vladislav Stolyarov