D:\sideБлогClojure

Я уже неоднократно упоминал Clojure. Но всё никак не расскажу, почему. Среднему программисту (пусть средний уровень и упал в последнее время довольно сильно) этот язык покажется очень странным и с точки зрения синтаксиса, и с точки зрения способа работы. Вот, собственно, и всё. Это очень странный язык. Поэтому я раз за разом к нему возвращаюсь. Да, потому что он такой странный. Странности будут подкреплены примерами.

Функциональщина

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

Например, в языке поощряется неизменяемость объектов. То есть, если вы не будете присваивать в переменную нечто, потом это нечто изменять, то Clojure вам мешать не будет. Что значит «как же иначе»? Ну, тут есть несколько путей.

Например, вы хотите цикл. Но зачем вы его хотите? Вы хотите проделать над чем-то действие N раз, например. Если выразить действие функцией f, то математически вы хотите

f(
  f(
    f(
      f(
        f(
          f(
            f(x) // Это ещё не Clojure, нет
          )
        )
      )
    )
  )
)

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

x, f(x), f(f(x)), f(f(f(x))), ...

Это последовательность значений. На самом деле, она бесконечна, но целиком она нам и не нужна – нам нужен всего один элемент, седьмой (считая с нуля: от момента, когда функция не была применена ни разу).

Такая последовательность выражена в Clojure правилом iterate. Это функция такая, она принимает функцию (то, что будет применяться) и первое значение, а возвращает последовательность. И это правило настолько простое, что его исходный код я могу написать прямо сюда:

(defn iterate
  "Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free of side-effects"
  {:added "1.0"
   :static true}
  [f x] (cons x (lazy-seq (iterate f (f x)))))

Одна строчка, не считая документации и самого объявления. «Вернуть последовательность, состоящую из Х и продолжаемую ленивой последовательностью, возвращаемой этой же функцией с той же f, но уже с f(x) в роли элемента». Звучит заумно. Здесь может разве что смутить слово «ленивой». Что это значит? То, что последовательность не нужно вычислять, пока явно не попросят. Ровно поэтому мы имеем полное право объявить бесконечную последовательность и не повесить компьютер.

Так а почему это круто? Потому что нам не нужно вдумываться в то, как заставить число в цикле принять нужные значения, чтобы цикл выполнился нужное количество раз. Программисты к этому привыкли, но это одна из тех привычек, которая вызвана несовершенством старых инструментов. Ну давайте. Сделаем с помощью iterate нужную последовательность и возьмём седьмой её элемент:

(nth (iterate f x) 7)

И всё. Коротко, по делу. «Взять N-ый элемент из {последовательности, полученной последовательным применением f к предыдущему, начиная с x}, где N это 7». И что забавно, эффект такой же, как если вы бы взяли переменную и последовательно сделали 7 раз x = f(x). Хотя признаю, это может выглядеть понятнее. Это вполне себе пример «декларативного программирования», где программа отвечает не на вопрос «как это получить?», а на вопрос «что это?».

Синтаксис и самодисциплина

Языкам семейства Lisp уже много лет. Clojure, возможно, самый молодой из них, и хотя в нём есть определённые расширения и допущения традиционного лиспа, в основе у него все те же «S-выражения»: пара скобок и набор содержимого. И такими выражениями делается всё: определения функций, вызовы функций, определения и импорты для пространств имён, макросы.

В погоне за лаконичностью Clojure обзавёлся некоторыми необычными выражениями, составляемые не просто круглыми скобками: объявление массива, мапа, короткой анонимной функции. И эти небольшие расширения сыграли только на руку лаконичности, разбавив круглые скобки и придав значения простым перечислениям каких-то штук.

И из кода сразу же понятно, какую структуру увидит компьютер. Ему же внутри приходится собирать и обходить из исходного текста программы синтаксическое дерево, чтобы что-то понять. Из этого, правда, вытек немного неприятный момент. Арифметика выглядит довольно странно:

(* 2 (+ 2 2 2) 3)

Но такая вещь, как приоритеты операций, просто теряет смысл. Их нет, они не нужны, потому что скобки обязательны. А ещё появилась возможность применять арифметические операции более (и менее!) чем к двум числам. Так что выигрыш тоже какой-то есть. Есть даже такие смешные случаи, как (+), применение плюса «ни к чему». Кстати, это 0. Догадайтесь, почему, и проверьте свою догадку: чему равно (*)? Ответ в конце поста.

Писать нечитаемый код на Clojure ужасающе просто. Синтаксис практически не накладывает ограничений: ставьте отступы, как хотите, а на запятые вообще наплевать. Можете хоть всё в одну строку писать. Просто соблюдайте скобочки. Отступы понадобятся только вашим коллегам и вам-самим-через-годик. Ладно, честно: принимаясь за Clojure, уже стоит иметь за плечами пару языков. Во-первых, нужен «вкус на читаемый код»: эдакая способность определять, насколько легко код читается случайно взятым программистом. Во-вторых, Clojure всё-таки требует довольно здоровой теоретической базы и/или сильного (очень сильного!) абстрактного мышления.

Творим с деревьями, что хотим

Разумеется, такая синтаксическая свобода дала ход разным DSL: «узким языкам». Таких довольно много на основе Ruby, поскольку синтаксис Ruby превосходно читается. Но чем подкупает Clojure? «Пустым» синтаксисом. Все (вообще все) ключевые слова DSL могут быть определены разработчиком этого DSL. И при этом никто не запрещает пользователю DSL пользоваться «ядром» Clojure, чтобы добавлять больше программируемости в этот DSL. В конце концов, любой код на Clojure – исполняемая программа. Или данные. Это как посмотреть. Лиспы как раз известны тем, что размывают эту грань.

Давайте, например, глянем на язык, состоящий исключительно из данных. На CSS, «каскадные страницы стилей».

body {
  font-family: 'sans-serif';
  font-size: 16px;
  line-height: 1.5;
}

А вот определение этого же кода на языке Garden: DSL под Clojure, описывающем CSS-конструкции.

(defstyles screen
  [:body
   {:font-family "sans-serif"
    :font-size (px 16)
    :line-height 1.5}])

Зачем? Затем, что в Clojure необязательно пользоваться только объявлениями из DSL: можно вносить туда изменения, можно определять собственные функции, переменные. Всё то же, что сделает для нас типичный CSS-препроцессор, вроде Sass, Less или Stylus. Только пользоваться при этом можно обычным языком программирования, на котором, в теории, можно написать и всё остальное тоже. А значит, можно передать в переменные и данные из основной программы без хитроумных API к препроцессору. Можно автоматизировать добавление т. н. «вендорных префиксов» для слишком новых свойств, которые без префиксов пока плохо поддерживаются.

Есть такая же штука и для HTML, называется Hiccup (странное название, кстати).

Но это всё преобразования в строчки, один к одному, без серьёзных премудростей. Всё это великолепие работает под JVM (да-да, под машиной Java), и генерирует строчки. Но создатели языка напряглись и сумели этот же язык собирать и в JavaScript. Та же синтаксическая гибкость, но теперь в виде, понятном браузеру. И то, что React пришлось решать с помощью собственного сорта JS, в котором можно писать на подобии HTML:

var app = <Nav color="blue"><Profile>click</Profile></Nav>; // JSX

В ClojureScript выглядит куда более однородно, хотя куда менее привычно:

(def app (nav {:color "blue"} (profile "click"))) ; ClojureScript

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

Но тёртые кложуристы пошли дальше, и выставили не только синтаксис Clojure в React, но и стиль работы. В итоге, по оценкам разработчиков этой вундервафли под названием Reagent, интерфейс (все эти панельки/кнопочки) потребляет даже меньше ресурсов, чем традиционный React, и при этом проще (!) в обращении из-за аккуратно спроектированного синтаксиса. Поэтому я не вижу причин использовать React, но не Reagent. Процесс сборки примерно одинаково сложен.

На инфраструктуре Java

У меня сложилось впечатление, что Java сейчас – «enterprise-язык номер один», на котором написано дикое количество решений по обслуживанию бизнеса. Есть свои решения по распространению библиотек, есть килотонны самих библиотек, успевай разбираться в интерфейсах.

И Clojure с его Leiningen (средством сборки) элегантно подцепился ко всей этой инфраструктуре. В Clojure можно пользоваться вебсервером Jetty, UI-фреймворком Swing, можно писать под Андроид (как говорят). И сам Clojure тоже является библиотекой, как ни парадоксально. Поэтому у проекта в зависимостях может стоять конкретная версия Clojure, которую Leiningen при старте может сам скачать и запустить проект именно в ней.

Про Java и JavaScript

В тексте выше может показаться, что я их путаю. Это не так. Clojure запускается на виртуальной машине Java, а ClojureScript компилируется в JavaScript. Вот такие дела.

Ответ на задачку сверху

Операция без аргументов возвращает такое значение, которое при применении с этой же операцией к Х даст в результате тот же Х. «Нейтральный элемент», как говорят математики. Таким образом, (*) это 1, поскольку что ни умножь на единицу, вернётся то же самое.