Как перестать бояться и полюбить Clojure

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привет, DOU, меня зовут Илья Дозоренко, работаю в IT 11 лет, на данный момент я Clojure Lead в компании Freshcode.

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

Цель статьи — рассказать об особенностях языка и, по возможности, дать мотивацию для изучения и использования Clojure.

История Clojure

Rich Hickey

Автором Clojure является Рич Хики (Rich Hickey) — гик-программист и культовая личность в среде Clojure-разработчиков. До создания Clojure Рич Хики преподавал C++ в Нью-Йоркском университете и работал в сфере информационных систем, сталкиваясь на практике со стандартными проблемами («в модели данных принадлежит ли песня исполнителю, или исполнитель — песне?»). В 2005м году Хики взял творческий отпуск для работы над личными проектами, а через два года опубликовал первую версию Clojure. На данный момент Рич Хики и компания Cognitect предоставляют коммерческую поддержку для Clojure.

Rationale, Reasonability

Рич Хики (Rich Hickey) дает такое краткое обоснование Clojure:

Зачем я создал еще один язык программирования? Мне просто был нужен:
— Лисп
— для Функционального Программирования
— реализованный на стабильной платформе
— с поддержкой параллельных вычислений

С подробной историей Clojure можно познакомиться в статье самого автора языка History Of Clojure.

В Clojure намного меньше новых и уникальных идей, чем кажется на первый взгляд. Конечно, в сравнении с Java Clojure выглядит мягко говоря экспериментальным языком, тем не менее Рич Хики разработал спецификацию языка полагаясь только на проверенные временем идеи и подходы.

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

Одной из основных идей Lisp является работа с кодом, как с данными (code as data). Это возможно благодаря тому, что код на языке Lisp состоит из s-выражений (symbolic expressions):

(A (B 3) (C D) (()))

s-выражение — это просто список, который состоит из значений или других списков. Для записи s-выражения используют префиксную нотацию, те оператор предшествует операндам, например, (+ 1 2 3).

Программа на языке Lisp идентична по форме её синтаксическому дереву (AST), такая особенность языка называется гомоиконичностью.

Концепция s-выражений открывает дополнительные возможности для мета-программирования — фактически, программный код на Lisp может изменяться во время его выполнения.

Clojure также привносит в Lisp некоторые крупные нововведения для актуализации языка для real-world задач, например, к списку добавляются новые структуры данных (maps, vectors, sets).

Немногим позже в реализацию Lisp были добавлена концепция макросов (macros) в виде дополнительной фазы преобразования кода (macro-expansion) перед интерпретацией. Макросы максимально упростили расширение синтаксиса Lisp для разработчиков.

Однако, мощь и простота Lisp одновременно является и его «проклятием» (The Lisp Curse), которое заключается в разрозненности Lisp-сообщества, обилии реализаций и отсутствии единых стандартов. С момента создания Lisp появилось множество диалектов языка (в частности, наиболее популярный — Common Lisp), которые тем не менее не получили широкого распространения по сравнению с современными скриптовыми языками вроде Python или Ruby.

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

Этапы изучения Clojure

В предисловии книги Clojure Applied Рас Олсен (Russ Olsen) говорит, что процесс изучения Clojure по большей части проходит в три этапа. Начальный этап — это изучение базового синтаксиса и принципов — как расставить скобки, зачем нужны квадратные скобки, чем отличается список (list) от вектора (vector) и тп. На втором этапе мы разбираемся, как это всё связать вместе — как собрать все функции высшего порядка в рабочий код, или как работать с неизменяемыми структурами данных. На третьем этапе вы начинаете исследовать экосистему Clojure — библиотеки и приложения — и пользоваться полученными знаниями чтобы разбираться в чужих решениях, тогда и начинается настоящий фан!

Знакомство с Clojure

Selenium тесты

Впервые я познакомился с Clojure в 2013, во время работы над Java EE проектом в сфере телеком (> 300K LOC). Основное веб-приложение только мигрировали на Java 7, в которой уже появился try-with-resources и NIO, но еще не было стрелочных функций (arrow functions), Java Stream API и jshell.

Не секрет, что Java считается языком с довольно избыточным (verbose) синтаксисом, который с переменным успехом помогают скрывать IDE, и упрощают добавлением новых API и синтаксического сахара в новых версиях. На данный момент актуальная версия Java 15, но согласно JetBrains в 2020 году 3/4 Java-разработчиков продолжают регулярно использовать Java 8, эта же версия используется в более чем 80% production приложений по версии New Relic.

Также Java плохо подходит для скриптовых задач, поэтому когда появилась задача покрытия функционала Selenium-тестами была предложена идея использования Clojure, по следующим причинам:

  • стабильная и знакомая платформа JVM
  • интеграция с Java — возможность переиспользования кода во избежание дублирования логики
  • Наличие REPL, возможность «набрасывать тесты на лету» в консоли
  • Простой синтаксис (например, в сравнении с Scala или даже с Java) должен позволить QA-инженерам самим править тест кейсы и добавлять новые

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

Сперва с помощью clj-webdriver были реализованы вспомогательные функции для навигации по веб-приложению и управления компонентами, после чего началась работа над тест-кейсами. Сейчас этим уже не удивишь, но в то время использование REPL серьезно ускорило написание тестов: тест-кейс можно было «набрасывать» на ходу прямо в консоли, без необходимости перезапускать сценарий, и это поверх JVM!

Ниже пример одного из тест-кейсов:

(ns project.tests
  (:use project.utils))

(defcase standart-create-qos "Создание политики качества"
  (select-main-menu "Управление SLA" "Политики качества")
  (press-button "Создание новой политики качества")
  (input "Название" "test_qos_2")
  (select-option "Класс обслуживания" "Class G")
  (select-option "Тип профиля" "test_profile")
  (set-quality-rule "В течение 15 минут" "коэффициент потери пакетов" "в прямом направлении" "менее" "10")
  (wait-response (press-button "Сохранить"))
  (check-exists-row "test_qos_2"))

Итоговые тест-кейсы на Clojure читались довольно просто, и выглядели привычно и.. императивно! Полученный DSL покрывал до ¾ кейсов, которые писали QA инженеры, для остальных случаев приходилось привлекать разработчиков.

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

Первый этап изучения Clojure довольно простой, и в этом заслуга минимального синтаксиса Clojure. Страхи и неудобство от скобок, которыми «пугают» новичков на самом деле сильно преувеличены. Для всех основных редакторов и IDE существуют paredit-плагины, позволяющие «балансировать» количество скобок (добавлять скобки парами и избегать случайных удалений) и поддерживающих structural editing (сочетания клавиш для манипуляции s-выражениями). Structural editing можно использовать как непосредственно для Clojure-кода:

так и для Hiccup-разметки:

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

public ByteArrayInputStream toInputStream (String s, String charset) 
  throws UnsupportedEncodingException {
   return new ByteArrayInputStream(s.getBytes(charset));
}

можно описать так в Clojure:

(defn string->stream [s charset]
 (-> s
   (.getBytes charset)
   (ByteArrayInputStream.)))

Для большинства Java-библиотек и модулей есть соответствующие библиотеки-«обвязки» в Clojure, поэтому можно переписать этот же код без Java-вызовов с использованием byte-streams:

(defn string->stream [s encoding]
 (to-input-stream s {:encoding encoding}))

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

Первый этап был пройден, синтаксис и основные конструкции усвоены, но когда Clojure «зацепил», то становится сложно остановиться, и приходится разбираться дальше!

Промежуточный этап

Сборники задач

Обычно, второй этап изучения — самый сложный. Должен сказать, что переход с императивного стиля Java на Lisp может быть непростым. Несмотря на минимальный синтаксис, поначалу часто приходилось «ломать голову» над различными конструкциями. На помощь здесь приходит большое количество ресурсов и интерактивных учебников, и конечно опыт и поддержка сообщества.

Своей элегантной простотой Clojure напоминает игру-головоломку вроде кубика Рубика, которая сперва смущает сложностью, но в которой приятно разбираться, когда ты понимаешь её принцип.

Дать пищу для размышлений и пройти этот этап мне очень помогли сборники задач вроде project euler, 4clojure и advent of code. Есть и более продвинутые учебные ресурсы, с игрофикацией процесса, красивыми «ачивками» и real-world задачами, но лично для меня главной мотивацией стал интерес при работе с Clojure, который заставлял искать ответы на вопросы и разбирать новые примеры.

Функциональный подход

Первое, на что обращаешь внимание при создании модели данных в Clojure — отсутствие привычного ООП. В Java мы привыкли описывать данные в полях классов, а логику работы с объектами — в методах. В Clojure функции не принадлежат данным (как методы — классу в Java), они обрабатывают данные. В качестве единицы инкапсуляции рассматриваются namespaces, которые соответствуют отдельным файлам.

Clojure разрешает использование нечистых (impure) функций, но поощряет функциональный подход, в частности использование функций высшего порядка (например для работы с коллекциями — map, reduce, filter, remove), а также композиции функций (partial, comp, juxt).

Неизменяемые структуры данных

В Clojure используются неизменяемые (immutable) структуры данных — это значит, что невозможно поменять значение в коллекции, можно только создать новую коллекцию с новым значением. При этом сохраняется алгоритмическая сложность доступа к объектам, например, доступ к вектору или hash-map — O(log32(N)), а для хранения используется практически константная память за счет разделения большей части внутренних данных между всеми версиями измененной структуры.

Концепция равенства объектов (equality) приобретает новый смысл — если две структуры равны, то они будут равны всегда, а не только в определенный момент времени. Этот подход серьезно облегчает работу с многопоточными приложениями, так как отсутствует необходимость использования thread-safe конструкций и локов (вспоминаем synchronized блоки и коллекции в Java).

Для хранения состояния приложения используются потокобезопасные ссылки (thread-safe references) — var, atom, agent, ref. Также в Clojure реализован механизм Software Transaction Memory, но это тема для отдельной статьи.

Ленивые вычисления

В Clojure используются ленивые (lazy) коллекции, это значит, что элементы коллекции не доступны заранее, а могут быть получены после вычисления.

Ленивые коллекции также позволяют описывать бесконечные коллекции. Для примера обратимся к задаче № 10 из сборника Project Euler: необходимо найти сумму простых чисел в интервале до двух миллионов.


(def primes
 (filter prime? (iterate inc 1)))

(defn solve []
 (->> primes
   (take-while #(< % 2E6))
   (reduce +)))

Здесь для краткости было опущено определение функции prime?. По-сути primes определяет всё множество простых чисел, тк iterate и filter — ленивые функции, и значение primes не будет вычислено до тех пор, пока мы не начнем обращаться непосредственно к элементам коллекции (если ввести primes в REPL, он зависнет на стадии evaluation). take-while — тоже ленивая функция, и только reduce непосредственно запускает вычисление при нахождении суммы.

Конечно, эту задачу можно решить и без использования ленивых бесконечных коллекций, и вы вправе выбрать например, подход с использованием for, но согласитесь, такая запись выглядит красиво и наглядно!

Data-oriented подход

В то время, как в Java декларативный стиль кода в WEB-разработке обеспечивает Spring и IoC, в Clojure декларативность частично обязана особенностям языка (гомоиконичность, неизменяемые структуры, макросы), а частично — сообществу, которое активно поощряет data-oriented подход в библиотеках.

В отрывке лекции «Clojure, Made Simple» Рич Хики объясняет преимущества data-oriented подхода в Clojure.

Для любого типа данных в Java используется промежуточный слой в виде интерфейса (например, class accessors/mutators или же getters/setters). Частично поиск нужных методов облегчается автокомплитом в IDE, плагин Project Lombok позволял автоматизировать их генерацию с помощью аннотаций в классах, а совсем недавно подобный подход (Records) был имплементирован в Java 14. Однако речь идет о базовых библиотеках, которые используются до сих пор, взгляните, например, на методы класса javax.servlet.http.HttpServletRequest:

Здесь разными цветами обозначены интерфейсы для доступа к трем внутренним полям, обратите внимание, они специфичны для каждого поля: разная логика названий — getParameterMap vs getHeaders, методы remove и set используются для attributes, в то время как для parameters они отсутствуют, и тд. Вместе с разнородным и громоздким интерфейсом получаем невозможность переиспользования логики манипуляции данными и сложности создания заглушек для тестирования (вспоминаем builders).

Clojure противопоставляет этому data-based подход, где объекты — это просто ассоциативные коллекции (maps) с открытым доступом ко всем полям:

Все функции Clojure по манипуляции данными можно использовать для работы с этим объектом. Потоковый макрос (threading macros) ->> позволяет компоновать функции так, как если бы вы использовали Stream API в Java, например:


(->> request 
 :headers 
 (filter #(str/starts-with? % "my-header")))

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

Продвинутый этап

Web разработка на Clojure

Спустя некоторое время у меня появилась возможность попробовать Clojure в веб-разработке, и таким образом перейти к третьему этапу изучения. Преимущественно я использую Clojure при создании классических информационных систем в небольших командах (до 10 человек), на мой взгляд небольшая команда позволяет полноценно использовать преимущества языка, одним из которых является быстрое прототипирование.

Ring. Быстрое прототипирование

Ring — это стандарт веб-разработки в Clojure. Аналогами Ring для Python и Ruby являются соответственно WSGI и rack. Репозиторий библиотеки Ring содержит спецификации request/response/middleware и базовый код для работы с ними, а также основные middleware, запуск Jetty-сервера и документацию. Все современные Clojure-фреймворки придерживаются совместимости со стандартом Ring, это позволяет без изменений запускать приложение на разных платформах — Jetty, http-kit, Immutant и др.

Простота работы с Ring практически стала откровением. Идиоматический подход с использованием концепции Middleware на основе функций используется сейчас во многих языках и фреймворках (например, Express в NodeJS). На тот момент у меня было несколько лет опыта работы с Java Servlets и Spring, поэтому в первую очередь сравнивал middleware-подход с Servlet Filters и Spring Boot.

К примеру, сравните пример базового REST-сервиса на Spring с аналогичным кодом на Clojure.

Мой первый REST API на Clojure выглядел фактически как в примере выше, разве что с использованием библиотеки compojure вместо более продвинутого compojure-api. В комбинации с REPL использование Ring позволяет очень быстро создавать рабочие приложения.

Compojure — это библиотека роутинга поверх стандарта Ring. Она позволяет описать функцию-обработчик Ring в привычном для нас и легко читаемом виде списка роутов:

(def my-routes
  (routes
   (GET "/foo" [] "Hello Foo")
   (GET "/bar/:id" [id] (str "Hello " id)))))

Библиотека темплейтинга Hiccup предоставляет DSL для описания HTML:


(html [:span {:class "foo"} "bar"]) ;; => <span class="foo">bar</span>

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

(html [:ul (for [x (range 1 4)] [:li x])])
;; => <ul><li>1</li><li>2</li><li>3</li></ul>

Этот же формат используется в популярной ClojureScript библиотеке Reagent (аналог ReactJS).

Web-фреймворки

Комьюнити предпочитает использовать композицию популярных библиотек вместо классических фреймворков «всё-в-одном» для облегчения разработки и для большей гибкости. Тем не менее для web-разработки в Clojure доступны как популярные темплейты, так и более продвинутые фреймворки:

compojure-api — compojure «на стероидах» для работы с RESTful-сервисами, в классический compojure добавлена поддержка clojure.spec и Swagger.

Liberator — библиотека для работы с RESTful-сервисами, использующая подход Erlang фреймворка webmachine. Позволяет описывать обработчики запросов с помощью концепции ресурсов, которые выполняются после прохождения запросом определенного графа решений (decision graph). Такой подход является более строгим и упрощает обеспечение совместимости со стандартом HTTP RFC 2616.

yada — как и Liberator, позволяет обеспечивать совместимость с HTTP-стандартом. Включает в себя Swagger, Websockets (aleph) и другие сервисы. Интересно, что yada в целом совместима с Ring, но не использует Ring Middleware, предлагая data-driven подход.

Pedestal — веб-фреймворк от Cognitect, в целом достаточно неплохой (кроме документации), но довольно opinionated (напр., использует свой роутер), отличается хорошей оптимизацией и совместимостью с веб-серверами (напр, возможность деплоя в AWS Lambda).

luminus — project template с определенным набором библиотек для работы с REST сервисами (Reitit, Swagger), Websockets (зависит от выбранного сервера), БД (HugSQL + Migratus), темплейтингом (Hiccup, Selmer), i18n (Tempura) и др. Активно поддерживается в настоящее время.

Jetty (включен в релизацию Ring) и http-kit — базовые веб-серверы для Clojure приложений, также поддерживают работу с websockets. Это standalone-библиотеки, те они будут упакованы в один JAR-файл с приложением.

Immutant — комплект, включающий веб-сервер Undertow и библиотеки для месседжинга (HornetQ), кэширования (Infinispan) и планирования задач (Quartz). Может быть использован как standalone-библиотека, так и в контейнере WildFly для кластеризации. Поддерживается JBoss Community.

Наиболее актуальный список библиотек, сгруппированных по назначению, доступен в Clojure Toolbox.

Расширяемость

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

Так, например, pattern matching реализован в отдельной библиотеке core.match. Можно сравнивать с образцом любые структуры данных Clojure:

(let [x {:a 1 :b 1}]
  (match [x]
    [{:a _ :b 2}] :a0
    [{:a 1 :b 1}] :a1
    [{:c 3 :d _ :e 4}] :a2
    :else nil))
;=> :a1

Или расширить сравнение для других типов данных:

(matchm [(java.util.Date. 2010 10 1 12 30)]
   [{:year 2009 :month a}] a
   [{:year (:or 2010 2011) :month b}] b
   :else :no-match)

core.async — библиотека, реализующая асинхронное взаимодействие с помощью каналов (очередей) и паттернов thread pool и inversion of control. Подход напоминает каналы в Golang, с тем основным отличием, что в Clojure каналы реализованы не в ядре языка, а в отдельной библиотеке.

(defonce log-chan (chan))

(defn loop-worker [msg]
  (println (str "hello, " msg "!")))

(go-loop []
  (let [msg (<! log-chan)]
    (loop-worker msg)
    (recur)))

(>!! log-chan "world")                ; => hello, world!
(>!! log-chan "core.async")           ; => hello, core.async!

Блок go (или go-loop, который совмещает в себе макрос go и функцию loop) — объявляет код, который будет выполняться асинхронно, в отдельном потоке, и сразу же возвращает управление.

Функция осуществляет парковку потока в рамках блока go-loop, то есть при получении сообщения выполняется функция-обработчик loop-worker, а затем блок приостанавливает выполнение, ожидая следующего сообщения.

С помощью блокирующей функции >!! в канал log-chan передаются строки, которые обработчик loop-worker выводит в консоль.

Для go-блоков core.async по умолчанию предоставляет фиксированный Thread pool размером в 8 потоков.

При этом, core.async активно используется и в ClojureScript. Реализация не содержит блокирующих операций и предоставляет идиоматический для Clojure подход к решению проблемы callback-hell.

clojure.spec и динамическая типизация

Динамической типизации Clojure в первую очередь обязан Lisp’у. Рич Хики также является сторонником использования динамической типизации по умолчанию.

О типизации годами ведутся долгие и сложные дискуссии, поэтому я опишу здесь только личные впечатления. Действительно, с использованием динамической типизации проще «выстрелить себе в ногу», но с другой стороны, становится гораздо проще стрелять в целом :) Отсутствие «статики» при web-разработке в Clojure компенсируется возможностью тестирования работы отдельных компонентов и всей системы «на лету» в REPL и покрытием функционала тестами, а также возможностью описания схемы данных и валидации с помощью core.spec.

core.spec использует data-oriented подход Clojure — для описания схемы данных используются структуры данных языка. Clojure делает смелое предположение о том, что во многих кейсах вам не понадобится прослеживание типов от начала и до конца выполнения модуля или цепочки функций. Вместо этого specs могут затем быть использованы в необходимый момент для проверки данных на соответствие протоколу, например, перед сабмитом веб-формы, или для проверки входных данных модуля/функции на корректность.

В примере ниже spec используется для валидации request/response и для преобразования типов (а также для генерации swagger docs, см. полный пример):

(s/def ::id int?)
(s/def ::name string?)
(s/def ::user (s/keys ::req-un [::id ::name]))

(def app
 (api
   {:coercion :spec}
   (GET "/users/:id" []
     :path-params [id :- ::id]
     :return ::user
     (ok (users-by-id id)))))

После нескольких лет использования статической типизации в Java, я поначалу чувствовал себя неуверенно при работе с гибкой информационной моделью в Clojure. Не буду лукавить, у статической типизации есть свои бесспорные преимущества — например, intellisense и оптимизации компилятора. Однако когда речь заходит об «отлавливании» ошибок в реальных рабочих кейсах, покрытие кода тестами и использование clojure.spec работает.

Действительно, иногда возникает обоснованная необходимость добавить типизацию, например, для сложного алгоритма или определенного модуля. Здесь на помощь снова приходит расширяемость Clojure: например, библиотека core.typed реализует полноценную статическую типизацию, которую можно добавить как для всей программы, так и для выбранных участков кода.

Другие возможности

Clojure активно используют не только в web и мобильной разработке, но и в сфере Data Science и DL/ML (см. например incanter, scicloj). Мне пока не приходилось непосредственно работать в этих областях, но я смог оценить элегантность Clojure в сфере работы с DSL.

DSL

DSL (domain specific language) — это язык с достаточно высоким уровнем абстракции, предназначенный для решения узкого класса проблем. В Clojure часто DSL описывают в терминах структур данных Clojure, как например Hiccup или honeysql:

{:select [:a :b :c]
 :from   [:foo]
 :where  [:= :f.a "baz"]}

Более кастомизируемым способом является использование парсеров произвольных грамматик.

Приведу реальный пример: при работе над Java-проектом, где для манипуляции фильтрами над датасетами в ElasticSearch использовался GUI, аналогичный searchkit, передо мной встала задача реализации «продвинутого» режима фильтрации. Этот режим позволил бы расширить возможности GUI фильтров с помощью SQL выражений и добавить более сложные логические конструкции, которые GUI не позволял использовать. Например, все условия в GUI фильтрах соединялись исключительно с помощью конъюнкции (AND) или дизъюнкции (OR), таким образом, в GUI было невозможно комбинировать операторы или расставлять приоритеты:

условие_1 AND (условие_2 OR условие_3).

Соответственно, задача состояла в преобразовании SQL-выражений такого типа в довольно сложный, но валидный запрос к ElasticSearch в формате edn, например, фильтр

NOT (project.amount <= 1000 AND project.name LIKE «%startup%»)

должен транслироваться в следующую форму:

{:query
{:bool
 {:must_not
  {:bool
   {:should
    [{:nested {:path "tuples"
               :filter {:bool {:must [{:term {:tuples.field_name_raw "project.name"}}
                                      {:multi_match {:query "startup"
                                                     :operator "and"
                                                     :lenient true
                                                     :fields ["tuples.str_value_e_ngram"
                                                              "tuples.str_value_ngram"]}}]}}}}
     {:nested {:path "tuples"
               :filter {:bool {:must [{:term {:tuples.field_name_raw "project.amount"}}
                                      {:range {"tuples.long_value" {:lte 1000}}}]}}}}]}}}}}

Основной запрос к ElasticSearch генерировался динамически и, как можно заметить, был довольно специфичен из-за структуры хранения данных, поэтому описать его с помощью SQL и выполнить с использованием ElasticSearch SQL JDBC driver было бы весьма затруднительно. Также требовалось реализовать автодополнение SQL выражения в UI (например, в зависимости от контекста отображать в выпадающем списке доступные поля датасета или операции над определенными типами данных — LIKE для строк и BETWEEN для numeric полей) и подсветку ошибок.

Для этой задачи как нельзя лучше подошел Clojure, особенностями которого являются гомоиконичность и простота работы с коллекциями. Идея состояла в том, чтобы (1) «распарсить» SQL-выражение в AST в виде структуры данных Clojure, которое затем (2) преобразовать в изоморфную структуру запроса к ElasticSearch.

Для первого шага была использована библиотека Instaparse, которая предназначена для создания парсеров произвольных грамматик описанных при помощи BNF: на вход подается выражение и BNF спецификация и строка с выражением , а на выходе — AST в формате hiccup (по-сути, вложенные Clojure-коллекции), при этом каждый узел дерева содержит также полезные мета-данные (например, соответствие строке/колонке исходного выражения), которые можно использовать для реализации автодополнения. При ошибке парсинга Instaparse также предоставляет указатель на строку и колонку с ошибкой и даже ожидаемые термы.

На втором этапе используется pattern matching вместе с функцией обхода полученного на первом шаге AST-дерева.

Для демонстрации подхода я подготовил упрощенный gist для вычисления арифметических операций. Разница с вышеописанным кейсом состоит в том, что после получения AST для математического выражения, мы преобразуем его непосредственно в код Clojure, который можно выполнить и получить результат. Для большей наглядности использование контекста и переменных в выражении пришлось опустить.

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

ClojureScript

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

ClojureScript — компилятор Clojure в JavaScript, использующий Google Closure compiler; термин также используется в качестве названия языка (иногда cljs). По-сути, cljs сохраняет все достоинства Clojure — структуры данных, функциональный подход, макросы и др., и отличается только нюансами взаимодействия с платформой, при этом предлагая production-ready экосистему и библиотеки. Использование связки Clojure backend + cljs frontend позволяет переиспользовать общий код, а shadow-cljs обеспечивает бесшовную интеграцию с пакетами npm. Наконец, cljs используется для создания мобильных приложений на React Native.

И о недостатках

Как и у любой другой технологии, у Clojure есть свои недостатки и компромиссы.

Из-за особенностей реализации персистентные структуры менее производительны, чем их аналоги в Java. В Clojure доступны и более производительные изменяемые (transient) аналоги для таких структур данных, как vector, map и set, но они не предназначены для конкурентного доступа. Также в Clojure можно использовать все возможности хост-платформы (например, структуры данных и библиотеки Java), тем не менее типичный код на Clojure вероятно будет выполняться медленней, чем типичный код Java или Scala. Это не критично для большинства приложений, но может стать решающим фактором при выборе технологии для специфических высокопроизводительных систем или модулей.

Новички часто указывают на достаточно высокий порог входа, но связано это как правило не с функциональным подходом, а с взаимодействием с платформой. Это не должно стать проблемой, если вы только не собрались сходу переносить на Clojure готовый Java-проект.

Тем не менее, я считаю, что один из самых серьезных недостатков Clojure — это скромный маркетинг. Clojure — простой язык с минимальным синтаксисом, продуманной экосистемой и большим потенциалом, он способен вас приятно удивить! Тем не менее, его публичный образ, по-видимому, унаследованный от Lisp, часто бывает связан с «мудрёностью» и отсутствием стандартов, хотя дела обстоят ровным счетом наоборот.

Как следствие, в изучение Clojure(Script) не решаются инвестировать новички, хотя на практике именно они овладевают языком быстрее остальных.

Статистика использования Clojure

Результаты опроса Stack Overflow Developer Survey 2019 свидетельствуют о том, что доля Clojure в общем рейтинге языков программирования (Programming, Scripting, and Markup Languages) составляет порядка 1.5%, что является неплохим результатом среди функциональных языков программирования (для сравнения, у Scala — 3.8%, F# не представлен в списке). Clojure находится на первой строчке рейтинга уровня зарплат, опережая F#, Go и Scala, при этом Clojure пользуется популярностью у наиболее опытных разработчиков.

Результаты исследования State of Clojure 2020 показывают стабильный рост процента компаний и разработчиков, использующих Clojure для рабочих проектов, и в частности для enterprise приложений.

Что делать новичку

  1. Как и с любой другой технологией — попробовать её на практике. Если вы новичок в функциональном программировании, начните с тренировки навыков на специализированных ресурсах или сборниках задач вроде Project Euler. В свое время я также использовал Clojure для реализации небольших пет-проектов, и Clojure придал этим проектам большой стимул.
  2. Учиться у других — обращаться за помощью к сообществу, литературы по Clojure и Lisp также масса (в конце указано несколько источников), встречаются и по-хорошему сумасшедшие вещи.
  3. Посмотреть лекции Хики, он доступно и интересно рассказывает об особенностях языка и о причинах тех или иных инженерных решений

Итоги

  • Clojure — интересный, активно развивающийся и доступный вариант функционального языка программирования для разработчиков (в том числе для Java-специалистов), которые ищут баланс между производительностью и fun-фактором
  • Clojure — универсальный язык, который используется как для full stack web и мобильной разработки, так и для создания скриптов или работы с DSL
  • Clojure, как потомок Lisp, нравится гикам, которые любят ставить перед собой сложные задачи и решать их. Но его роль более глобальна — он был создан для того, чтобы сделать разработку проще, и по-моему мнению с этой задачей он справляется отлично.

Полезные материалы

Почему стоит изучить Clojure?
Clojurians — Slack Clojure-сообщества с каналами для новичков (beginners, clojure, clojurescript) и поиском работы (jobs, remote-jobs)
/r/Clojure — Clojure сабреддит с анонсами и обсуждениями библиотек/лекций и др.
Clojure Toolbox — актуальный справочник Clojure(Script)-библиотек
Rich Hickey fanclub — коллекция лекций и интервью Рича Хики

На clojure.org перечислены все известные книги по Clojure, ниже несколько книг по изучению языка, которые я могу порекомендовать:

Programming Clojure — хорошая вводная книга, недавно вышло обновленное третье издание (не путать с Clojure Programming — тоже прекрасная книга-справочник, но уже немного устаревшая)
Clojure Applied — более продвинутая книга для тех, кто уже знаком с Clojure и FP
Mastering Clojure Macros — для тех, кто желает совершенствовать свои навыки написания макросов

👍НравитсяПонравилось9
В избранноеВ избранном7
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Олександр Соловйов — Чому Clojure
www.youtube.com/watch?v=ggjQnfC2Xts

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

Lisp не взлетел. Clojure, как потомок Lisp — не взлетит тоже. По тем же причинам.

меня эта статья вообще удивила.
я помню когда был в Джаве, Кложа обсуждалась с хайповым придыханием.
Наобсуждались, кто-то и попробовал, поигрался. Оракл взялся активнее развивать Джаву, и ... интерес прошел. Было это наверное лет 5, а может 7 тому.
И вот, призыв полюбить Clojure...

Джинни — 8 вакансий
ЗП калькулятор — 0 анкет

Спасибо, пожалуй буду любить кложур на расстоянии)

Джинни — 8 вакансий
ЗП калькулятор — 0 анкет

А сколько вакансий в «профильных тусовках»? И на какую зп?

Фишка всем «маргинальных технологий» в том что там людей ищут не через открытый рынок, а через тусовки единомышленников.

Очень крутая перспектива — чтобы найти работу нужно попасть в какой-то кружок единомышленников ахаха

Очень дорого бизнесу будет обходиться такой фетиш)

Никто кложу в бизнес и не тянет. А на «тусовках единомышленников» — ищут дурачков, желающих поработать на каком-нибудь очередном бесплатном оупен-соурсе, забесплатно.

Очень дорого бизнесу будет обходиться такой фетиш)

Нада считать в каждом конкретном случае:
С одной стороны выбор людей меньше, с другой эти люди уже отфильтрованы и конкуренция работодателей меньше.
И так же надо понимать о какой доменной области идет речь:
— если «очередной инетмагазин», то конкурировать с джава/питон/пхп как-то сомнительно;
— если какой-то специфический процессинг фин данных, то тут важнее понимание доменной области чем конкретный язык (ту же кложуру можно за пару вечеров подтянуть).

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

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

я вообще не могу понять как в мире, где существуют Scala и Haskell может быть место такому гм... функциональному руби на JVM.

какие задачи решает кложур, которые не решаемы на функциональных языках с нормальной, человеческой типизацией?

какие задачи решает кложур, которые не решаемы на функциональных языках с нормальной, человеческой типизацией?

Написание нормального кода?

нормального кода без типов. ясно.

нормального кода с из одного типа не бывает.

ruby активно позиционируется как developer-friendly language, но лично мне не совсем понятно, чем это обусловлено (синтаксис? объектная модель?). тогда как в clojure это четко видно: минимальный синтаксис + data-oriented подход + FP + dynamic typing в сумме дает продуктивность.

мне нравится Haskell, я немного работал с ним в качестве хобби, с Scala я практически не работал. я считаю, что Clojure более прагматичный язык, чем Haskell, при менее высоком пороге входа, при этом у них сопоставимый defect rate (см. Functional-Static vs Functional-Dynamic):

cacm.acm.org/...​uality-in-github/fulltext

а мне непонятен closure => мы друг друга не поняли

Отличная статья и пища для раздумий)

кложа статическая строго типизированная? если нет то втопку

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

Какбэ вся суть функциональщиков: сам придумал проблему, сам решил ее.

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

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

хотелось бы больше преимуществ кложура для бизнеса

Вот челик пилит большое приложение для хелс индустрии

www.youtube.com/...​vj4JrNA&feature=emb_title

Вот челик пилит большое приложение для хелс индустрии

Ну бывает. Где преимущества? Что дала кложура по сравнению с какой-то джавой?

Так про это и доклад. Этот и другие от того же автора.

Как сказал один МД: меньше кода — меньше багов © ;)

Можно смеяться, но с этой парадигмой он сделал великие изменения в одном крупном банке

Как сказал один МД: меньше кода — меньше багов © ;)

Тогда берите спринг и вешайте анноташки :)

Ещё пример знаю Одину дата ориентированную компанию, которая пишет на Clojure — менеджер утверждает что выкатывают на гора раза в два больше чем на джава в предыдущей его конторе (а она не богадельня и работают там много)

> все же хотелось бы больше преимуществ кложура для бизнеса (какие проблемы он решает по сравнению с мейнстрим языками?)

не совсем моя компетенция, но всё же попробую ответить. в сфере веб-разработки влияние Clojure на бизнес скорее опосредованное, и заключается в подборе кадров/команды. по моему мнению, Clojure сейчас обладает качествами, схожими с теми, которые успешно использовал Python — general purpose язык, с довольно крупным и активным комьюнити и мотивированными и талантливыми разработчиками, и это делает его привлекательным для нанимателей. и это хорошо заметно по их отзывам, вот например небольшая подборка бизнес-стори различных компаний использующих Clojure для своих продуктов (там и веб, и ML): juxt.pro/clojure-in

по всей видимости, Clojure не получит популярности Python, тк для этого понадобился бы какой-то невероятный success-story и спонсорство от крупной компании вроде Google или Facebook, но растущее (без влияния каких-либо крупных спонсоров) сообщество Clojure — это полностью заслуга простоты и экспрессивности этого языка. выстрелит ли Clojure в других сферах, и получит ли какую-то киллер-фичу — зависит как от сообщества, так во многом и от Cognitect (тк им принадлежат права собственности)

с довольно крупным и активным комьюнити и мотивированными и талантливыми разработчиками

лол просто лол

по всей видимости, Clojure не получит популярности Python, тк для этого понадобился бы какой-то невероятный success-story и спонсорство от крупной компании вроде Google или Facebook

1) Вы выше писали про «мотивированных и талантливых». Почему нет саксес-стори, типа рейлс для руби?
2) Питончик стал популярным потому что заменил, морально устаревший перл и вообще идеально подходил для несложного скриптинга (который нужен не только в системных скриптах, но и в современном вебе и МЛ). Питон — отвратительный язык, но у него есть ниша. Где нише кложуры так и не ясно.

сообщество Clojure — это полностью заслуга простоты и экспрессивности этого языка.

Эта заслуга того что это единственная живая экосистема чего-то лиспоподобного + доступ ко всей экосистеме джава (интероп с джавой было их килер фичей лет 10 назад)

Питон — отвратительный язык, но у него есть ниша.

Можете чуть раскрыть тему ?

Можете чуть раскрыть тему ?

Про нише как бы написал.
Про отвратительный — хотя бы посмотрите описание методов в классах, или на перегрузку операторов, или на прекрасную «функцию» len

Про отвратительный — хотя бы посмотрите описание методов в классах

Имеется ввиду передача self ? Так то плюс минус все как у всех.

или на перегрузку операторов

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

прекрасную «функцию» len

В плане что она может возвращать длинну строки либо длину списка?

Какбэ вся суть функциональщиков: сам придумал проблему, сам решил ее.

Можешь с этим человеком поспорить про функциональщину
mobile.twitter.com/5HT
Посмотрим, хватит ли у тебя интеллекта вести дискуссию)

Посмотрим, хватит ли у тебя интеллекта вести дискуссию)

У меня достаточно интеллекта не вступать в дискуссию с персонажами типа Максимки :)

Just a side node, вивід типів а-ля хіндлі-мільнер дозволяє писать такий же лаконічний не заблоачений аннотаціями код. Гарантуючи при цьому залізнобетонну статичну типізацію. Це навіть круче ніж писать тести чи спеки, бо можна звірятись в процесі написання з тим, що про код думає компілятор. Якщо типи не сходяться — одразу десь помилка.
Мені особисто після ML-family (ocaml, rescript) функціональних мов кложа щось зовсім не заходить. Основна причина — не зрозуміло що відбувається в коді через динамічну типізацію та магію з списком параметрів. Ну і також абсолютно не радує патіхард зі скобочек а-ля «}]]))]]}».

Из практики с Haskell и Clojure:
1. Типы приложения тоже нужно проектировать. Чем больше свойств софта мы хотим выразить в типах — тем сложнее будет его писать. Некоторые свойства вообще требуют Advanced системы типов которых даже в GHC нет.
2. Типизация добавляет приключений с сайд эффектами. Приходится придумывать монады, монадические трансформеры.
3. На Clojure цикл write-run-change значительно быстрее за счет REPL(да, в Haskell тоже есть REPL, но опыта подключения к работающему софту и замене целых частей кода на лету у меня нет)
4. Скобочки перестаешь замечать уже через 2 недели. А если работаешь с paredit режимом в Emacs — начинаешь жалеть что другие языки не имеют такого простого и мощного синтаксиса.

Справедливости ради это касается только моего опыта с Haskell. С ocaml и rescript(и другими воплощениями типа purescript) я не работал

Ну хаскель — це інша сторона медалі, там від теоркату нікуди не дінешся.
В окамлоїдах без теоркату можна чудово жити, і це мене надзвичайно сильно сподобалось. Точніше там є наприклад монади і апплікативи, але вони дружні і використовуються лише там де треба. Парсер комбінатори наприклад пишуться досить елегантно з новим let-syntax.
Окамлоїди звичайно мають свої фатальні недоліки в плані синтаксису (паредіт це однозначно жирний плюс кложі в порівнянні з let..in скобками окамлу) та наноскопічному ком’юніті, але в мене вони викликали стійке враження «якщо взяти це за основу і копать в тому напрямку, то з часом можна створити щось дійсно круте»

Интересное замечание про комьюнити.

Размеры комьюнити сильно связанны с продакшеном. И у меня есть стойкое ощущение что Clojure больше используется в продакшене чем другие «альтернативные» языки(возможно это confirmation bias).

Могу выделить несколько причин/гипотез:
1. JVM + экосистема
2. Дизайн заточен на практическое применение
3. Простота и композируемость(expression problem и мультиметоды как варианты решения)
4. Простой monkey-patching. Не раз спасал в продакшене

Так что, при всей моей любви к строгой сильной типизации — на свой проект я бы брал кложу

Ага, обираючи між окамлом та кложею для веб продакшону, я би теж обрав кложу. Там дійсно більше ком’юніті, як мінімум за рахунок неміряної тучі джавістів. Ну і джава-ліби всі є, залишилось лише написать обгортку.

Ага, обираючи між окамлом та кложею для веб продакшону, я би теж обрав кложу. Там дійсно більше ком’юніті, як мінімум за рахунок неміряної тучі джавістів. Ну і джава-ліби всі є, залишилось лише написать обгортку.

для этих вводных есть куда более похожая альтернатива ocaml в качества F# со статической типизаций и интроп c дотнетом как на уровне либ так и языка куда более продвинутый в обе стороны (C#/F#) чем то, что я смотрю предоставляет closure в случае с java. как большой плюс позволяет писать лаконичный декларативный код для продакшина не взрывающий мозг ООПшникам.

О! Годнота подъехала, смотрю)
Кстати, можно еще упомянуть про ClojureCLR (кложура для дотнета, бинарники можно скачать с sourceforge.net/...​rojects/clojureclr/files ), хотя данная реализация скорее больше для опытных кложуристов)

З.Ы.

Однако, мощь и простота Lisp одновременно является и его «проклятием» (The Lisp Curse), которое заключается в разрозненности Lisp-сообщества, обилии реализаций и отсутствии единых стандартов. С момента создания Lisp появилось множество диалектов языка (в частности, наиболее популярный — Common Lisp)

Вроде как Common Lisp и есть стандарт, другое дело, что реализаций коммон лиспа несколько (ClozureCL, CMUCL, SBCL, Clisp, ECL, Corman Lisp, и др.), но и среди реализаций коммон лиспа есть явный (насколько понимаю) лидер — SBCL. Хотя конечно есть и нестандартные диалекты лиспа (типа AutoLisp, Elisp или newLISP), но они по-моему сугубоо узко-специализированные (например, Elisp он заточен чисто под емакс, AutoLisp под автокад, и кроме своих программ эти диалекты нигде больше не используются).
Вот плачевнее ситуация со стандартами и сообществом в Scheme (ИМХО), ну и куча диалектов самой схемы (и в разных реализациях Scheme могут использоваться разные стандарты, хотя есть последний стандарт R7RS), которые могут сильно различаться (наиболее популярные, если не ошибаюсь, Chicken Scheme, Guile и Racket).

В тему ClojureCLR — есть интеграция с Unity (гейм дев)
arcadia-unity.github.io

Подписаться на комментарии