Mikhail Kuzmin

Clojure: Динамическая разработка

Меня часто спрашивают, почему я программирую на Clojure. Программисты из сообщества часто скажут, что им нравится программировать в функциональном стиле или работать с данными вместо классов и объектов. А я, учитывая мой коммерческий опыт разработки, поставлю на первое место динамическую разработку.

Суть Clojure в том, что мы можем взаимодействовать с запущенной программой. Мы можем узнать, с какими данными сейчас работает наша программа, заменить любую функцию и мгновенно оценить результат. Не нужно терять кеш, разрывать соединения с БД или перезапускать веб-сервер. Все это позволяет быстро прототипировать и проверять гипотезы.

Например, в GMonit мы собираем данные с объектов мониторинга с помощью сторонних агентов. Даже если есть документация или спецификация на агент, постоянно возникают ситуации, когда проще взять и посмотреть на передаваемые прямо сейчас данные. И не просто посмотреть, а проанализировать: разархивировать, декодировать, собрать статистику, сравнить с другими данными. Или я могу взять java.nio.ByteBuffer из стандартной библиотеки Java и написать функцию для формирования компактного ключа Redis в виде массива байт, маленькими шагами и постоянно проверяя результат.

Clojure, как и многие другие языки, имеет REPL. Даже Java теперь имеет REPL. REPL - это аббревиатура от “Прочитать, Исполнить, Распечатать, Повторить”. REPL - как правило, это особый режим работы интерпретатора, когда мы загружаем программу в особое окружение и можем взаимодействовать с ней с помощью командной строки. Как правило, мы можем вызывать функции, создавать объекты и т.п.

Опыт работы с REPL в Clojure, как и в остальных LISP языках, на мой взгляд больше похож на опыт использования ноутбуков вроде Jupyter Notebook или Observable HQ. В ноутбуках у нас есть зависимые ячейки с кодом, которые делают вычисления, а результат выводится рядом с соответствующей ячейкой. В Clojure редактор подключается к REPL не в виде еще одного окна, а прямо в область редактирования. Мы можем выражение за выражением, шаг за шагом, написать всю программу, сразу получая обратную связь.

Если в Java или Ruby у нас есть по отдельности концепции языка программирования, редактор/IDE и REPL, то в Clojure все эти три части работают вместе на программиста.

К слову, схожий подход иногда применяют даже для C. В одном из своих докладов Андрей Аксенов, автор Sphynx, советовал писать код прямо в отладчике GDB.

Когда я 7 лет назад начал знакомиться с Clojure после Ruby, я не понимал, как это может работать. Я не сразу понял, что как бы странно это не звучало, что Clojure - это компилируемый язык. Оказалось все довольно просто и остроумно.

Есть термин - единица компиляции. Если мы работаем с Go, то единицей компиляции будет вся программа. В результате мы получаем один большой исполняемый файл. Если мы работаем с C или C++, то, как правило, мы компилируем каждый файл по отдельности, а потом линкуем их вместе. И единицей компиляции будет файл. А еще мы можем разбить всю программу на множество плагинов. И тогда единицей компиляции будет плагин, в виде динамической библиотеки.

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

У Clojure единица компиляции - не программа, не файл, а одна функция. Т.е. мы работаем с минимально возможной единицей компиляции.

У этого подхода есть одна особенность: компилятор выполняет оптимизации только в рамках единицы компиляции. Т.е., у нас нет способа оптимизировать всю программу, состоящую из множества плагинов, целиком. И если бы Clojure компилировала функции сразу в машиный код, то мы бы потеряли целый пласт оптимизаций, включая подстановку функций (function inlining). К счастью, Clojure - hosted язык, и компилируется не в машинный, а Java байткод. И уже JVM в процессе исполнения, накапливая статистику, компилирует (JIT) разрозненные функции в эффективный машинный код.

В Clojure все построено вокруг динамической разработки и динамической компиляции. Все способствует эффективной работе программиста: наискорейшему получению обратной связи от изменений программы.