Волшебство Clojure

Когда-то в детстве мне посчастливилось увидеть в одной детской энциклопедии картинку. Справа был небольшой фрагмент кода на бейсике. На таком старом классическом бейсике, с нумерацией строк через десять, вроде 640, 650, 660. А слева был монитор, на котором отображалась монохромная картинка то ли взлетной полосы, то ли самолета, то ли самолета, взлетающего со взлетной полосы… и подписано, что, мол, программка справа рисует картинку слева.

Сказать, что я был поражен — не сказать ничего. Мне тотчас же захотелось овладеть этим волшебством. Я тоже хотел делать такие картинки! Я тоже хотел писать тексты, которые никто не понимает, но которые делают что-то… настоящее.

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

Первые шаги

Впервые я услышал про Clojure лет 7 назад. Кажется версия тогда была то ли 1.1, то ли 1.2… К тому времени я уже знал, что LISP — это очень круто. Знаете, мне кажется в жизни каждого программиста (за исключением разве что тех, кто начинал с LISP), наступает такой момент, когда он узнает про LISP и понимает насколько это КРУТО. Это круче чем все, что он знал о программировании до этого. Это как раз то, каким должен быть язык Настоящего Программиста. Это чуть ли не единственный действительно Мощный Инструмент. Ну а после чудесного прозрения программист идет дальше писать свой C++/Java/whatever.

Года три назад я снова возвращался к Clojure. Я пытался читать Clojure for the Brave and True и The Joy of Clojure. Не то, чтобы у меня это очень хорошо получалось. Помню, разделы про коллекции шли с таким скрипом… Неподготовленному глазу тяжело выцеплять конструкции в незнакомом языке. Это справедливо для всех языков, не только для Clojure. Просто в случае с Clojure это более заметно.

И вот совсем недавно, когда я решил, что все, пора уже идти в веб, я снова взялся за Clojure. На сей раз я уже пытался что-то писать. И на сей раз я уже успел немного попрактиковаться в ФП на Scala. И уже имел некоторый опыт с Emacs, так что Cider теперь не казался таким уж страшным. И все пошло куда бодрее! Есть только одна проблемка с этим языком: после него все другие языки кажутся несколько… ограниченными. Я предупредил!

Очаровательные шестеренки

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

Так вот, с Clojure этот момент наступает чертовски быстро! Довольно быстро становится понятно, что у языка крайне простые внутренности. И они все доступны для исследования. Мне это чем-то напомнило Python. Помню свое потрясение от вещей вроде __dict__ — для человека, пришедшего из статического мира C++/C# это было крайне дерзко. Уровень рефлексии, который предоставляет C++… примерно нулевой. C# имеет мощную рефлексию, но он по прежнему крайне статический – ничего нельзя добавить во время выполнения (ладно, можно, если вы не прочь написать пару страниц кода). А в питоне рефлексия не просто на чтение – весь язык как на ладони!

Но при этом в питоне у нас есть МИЛЛИОН мелких деталей, описывающих все эти хитросплетения специальных методов, полей, встроенных типов и прочего. А в Clojure всего этого нет. В Clojure есть несколько базовых концепций, из которых разворачивается все остальное. Нет, буквально. Вещи, которые могут показаться базовыми, тупо определяются в core.clj. Продолжая сравнение с питоном: в питоне мы имеем кучу встроенных типов, реализованных на C. В целом, в стандартной библиотеке питона тенденция такая: напишем что-то на питоне, если приживется, перепишем на С для пущей эффективности. А Clojure, напротив, стремится сократить объем магии (т.е. того, что реализовано на нижележащем языке) к минимуму и сделать как можно больше на самой Clojure.

И это имеет занятные последствия. Например, помимо стандартного для динамических языков получения документации по функции прямо из REPL’а, в Clojure можно получить еще и код. Интересно, как в Clojure реализовано сравнение? Пожалуйста:

Подобная фича имеет смысл только тогда, когда большая часть языка реализована на самом языке. Даже если бы мы включили исходный код C-модулей питона в питон и сделали бы, чтобы help() выводил этот код… какой в этом смысл для Python-программиста? C-код питона слишком сложный, чтобы лезть в него без особой нужды. А вот вывод макроса (source) вполне себе читаем для Clojure-программиста. И он может быть весьма полезен! Видите, что используется некий recur, но не знаете, что это такое? Не вопрос: (doc recur).

Подобные исследования очень занимательны при изучении языка. Читаешь про какие-нибудь возможности, становиться любопытно, как это реализовано. Идешь смотреть код, замечаешь там еще какую-нибудь функцию. Смотришь и ее код. И так далее. Весьма увлекательное занятие!

REPL без границ

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

Это присуще всем динамическим языкам? Ну… в некотором объеме – да. Вот, например, питон1. Там же ведь тоже есть REPL, тоже можно интерактивно экспериментировать, да? Можно. Но вот именно что экспериментировать. Когда пытаешься работать с настоящим проектом, REPL питона превращается в ад, а from importlib import reload становится вашим лучшим другом. И этот reload не всегда помогает. В питоне очень легко “сломать” REPL, т.е. завести его в такое состояние, что проще выключить и включить заново. Это утомляет. Это разрывает поток. Это заставляет не доверять REPL’у ничего серьезного. Потому что знаешь, что очень скоро его опять придется обнулить.

А в Clojure в REPL можно сидеть хоть целый день2. Из него прекрасно запускается development server, перезагружающий код на лету. Из него же запускаются тесты. И все это прекрасно сосуществует вместе! Можно сохранить файл и он перезагрузится. А можно просто отправить кусок кода (скажем, определение функции) в REPL – и он выполнится. И тогда при обработке следующего запроса уже будет использована эта новая версия функции. Можно, в конце концов, просто быстро что-то набрать в REPL и получить ответ. Честно говоря, после этого любой другой стиль разработки кажется немного неполноценным…

Конкретно удобство итеративной разработки в REPL’е – это, пожалуй, следствие некоторых дизайнерских решений, таких как обязательная косвенность в обращении к var’ам или устройство пространств имен. Но есть еще одна штука, которая делает REPL не просто удобным, а волшебным. Может быть я слишком романтик, но у меня подобное устройство языка вызывает определенные эмоции. То самое ощущение работающей штуки, которую можно изучать.

Я говорю про… отсутствие границ. В Clojure одна часть системы перетекает в другую, это происходит плавно, без резких скачков. Что я имею в виду? Ну вот смотрите, возьмем какой-нибудь классический компилируемый язык. Скажем, Scala. Как мы работаем с этим языком? Пишем программу в виде текста, потом отдаем ее компилятору, получаем на выходе набор Java-классов, которые можно скормить JVM. И здесь у нас есть очень четкая граница между текстом и набором классов. Между тем, что в файле и тем, что есть во время выполнения. Чтобы перепрыгнуть эту границу, нужно запустить компилятор. Который, вообще говоря, сам по себе. Он – отдельная тулза, он не часть вашей системы.

Динамические языки чуть ближе к “безразрывному” идеалу, но все равно не дотягивают до него. Да, интерпретатор всегда под рукой, да, мы можем eval(). Но что мы передаем в eval()? Опять текст. А что у нас в работающей программе? Объекты. Функции. Переменные. Но не текст.

А в Clojure этого разрыва нет. На вход компилятору идут структуры данных. И наша программа оперирует такими же структурами данных! Макрос от обычный функции отличается разве что тем, на какой стадии он вызывается 3. Получается, что компилятор и моя программа работают бок о бок. Они не просто находятся в одном процессе, они говорят на одном языке! Им не нужно общаться через строки. Вот именно это создает Волшебство. По крайней мере для меня.

Про Рича Хикки и простоту

Как можно говорить про Clojure и не поднять эту тему? Лично для меня это было одним из факторов, подтолкнувших именно к этому языку. Рич Хикки – чрезвычайно харизматичный человек и очень опытный разработчик. Как по мне, он говорит очень правильные вещи. Но вот что забавно: более-менее понимать о чем он говорит я начал только после того, как сам попробовал что-то поделать на Clojure. То есть, я всегда считал, что понимаю. Ну, елки-палки, конечно простота — это круто и правильно, о чем тут спорить! Все ведь согласятся? Ну и пойдут дальше писать свои делегирующие абстрактные фабрики. Или тесты о пяти моках. Получается прямо как с божественным LISP’ом.

А вот уже немного поковырявшись в иммутабельных мапах, я с удивлением обнаружил, что мне этого не хватает в других языках. В них непременно нужно куда-то идти и описывать структуру. В лучшем случае — кратко (case classes, named tuples). В худшем – городить портянку бойлерплейта (C#, Java, whatever…). Из известных мне языков ближе всего к идеалу Clojure подобрался, пожалуй, JavaScript — в нем есть объектные литералы, которые работают как словарих4. И при этом их можно использовать с удобным data.key вместо data[‘key’].

То есть, до тех пор, пока я не поработал в таком стиле (а в Clojure ты просто вынужден работать так, других вариантов нет), то я даже не задумывался о том, что можно по-другому. Что не обязательно объявлять структуру на каждый чих. Тот самый data-oriented programming, о котором говорит Рич, — это то новое, что показала мне Clojure.

Впрочем, я не знаю, как Clojure проявляет себя в настоящих проектах. Я написал пару сотен строчек кода и еще даже ни разу не использовал ни мультиметоды, ни протоколы, ни хваленую многопоточность. Так что, не мне судить об этом. Но я точно знаю, что как игрушка Clojure просто потрясающа! Если вы подумываете о том, что Clojure — хороший язык, в котором реализованы правильные идеи и что, пожалуй, надо бы его изучить… подумайте лучше о том, что это прикольный язык. Это язык, который может напомнить вам, как сильно вы любите программировать.

Примечания

  1. Да-да, питону сегодня много достается, но это лишь потому, что это единственный динамический язык, с которым я имею более-менее приличный опыт.
  2. Это, конечно, немного преувеличение. Так, репл в Clojure приходится перезапускать как минимум при добавлении зависимостей, что на первых порах разработки происходит довольно часто.
  3. Матерым Clojure’истам: я не знаю деталей и еще не писал макросы самостоятельно, так что могу ошибаться, извиняйте.
  4. Вроде бы в Lua используется такой же подход, но что можно делать на Lua?

Leave a Reply