Как я написал web-приложение, используя только clojure

1b1ade0cbebf09ac91c81921b0fe4728.pngНедавно я познакомился с интересным языком — clojure. Мне сразу понравились ленивые и иммутабельные коллекции, stm, макросы, обилие скобочек и dsl на все случаи жизни.И я решил попробовать сделать web-приложение, используя только clojure.

Приложение Было задумано создать простую искалку субтитров, которая: каждые 5 минут индексирует новые субтитры на addicted, notabenoid и других сервисах; имеет одностраничный web-интерфейс с поиском без перезагрузки страницы; показывает в web-интерфейсе количество проиндексированных субтитров и меняет его при появлении новых; имеет простое api для взаимодействия с десктопным клиентом. Парсеры На удивление парсеры было писать просто и удобно. Сначала казалось, что скобочек уж очень много, но threading макросы (→, →>, -<> и -<>> — передача результата аргументом следующему выражению) очень помогали.Например, кусок парсера notabenoid, делающий одно и тоже на python и clojure:

clojure python (defn get-release-page-result «Get release page result» [page] (-<>> (get-release-page-url page) helpers/fetch (html/select <> [: ul.search-results: li: p: a]) (map (helpers/make-safe book-from-line nil)) (remove nil?) (map episodes-from-book) flatten)) def get_release_page_result (page): »«Get release page result»« url = get_release_page_url (page) content = requests.get (url).content soup = BeautifulSoup (content) for line in get_lines_from_soup (soup): book = get_book_from_line (line) if book: yield from get_episodes_from_book (book) 16 скобочек 14 скобочек Для запуска парсеров используется библиотека at-at, для парсинга html — enlive. Результат записывается в elasticsearch.Серверная часть Сервер Как сервер я выбрал http-kit, в основном из-за того, что мне захотелось web-сокетов. И их тут очень просто использовать, например, отправка всем клиентам количества проиндексированных субтитров после обновления будет выглядеть так: (add-watch total-count: notifications #(doseq [con @subscribers] (send! con (prn-str {: total-count %4})))) Роутинг Для роутинга — compojure. Тут нет никаких отличий от django и других популярных фреймворков: (defroutes main-routes (GET »/» [] (views/index-page)) (GET »/api/list-languages/» {params: params} (api/list-languages params)) (GET »/notifications/» [] push/notifications) (route/resources const/static-path)) API Так как мы везде используем clojure, то наше api должно возвращать результат в родных структурах данных и в json (для десктопного клиента на python). Библиотеки, которая так может, я не нашёл (уже нашёл), поэтому пришлось немного повелосипедить и изобрести свой мини-dsl: (defn- get-writer «Get writer from params» [params] (if (= (: format params) «json») json/write-str prn-str))

(defmacro defapi «Define api method» [name doc args & body] `(defn ~name ~args ((get-writer (first ~args)) ~@body))) И как простой пример использования: (defapi list-languages «List all available languages» [params] (models/list-languages)) View Для рендеринга html я воспользовался специальным dsl — hiccup, шаблон с ним выглядит немного «марсианским»: (defn index-page [] (html5 [: head [: title «Subman — subtitle search service»] [: body [: h1 «Welcome to subman!»]])) Стили Для стилей в clojure тоже есть свой dsl — garden. Код с ним выглядит тоже странно: (defstyles main [:.search-input {: z-index 100 : background-color »#fff»}] [:.info-box {: text-align «center» : font-size (px 18)}] [:.search-result-holder {: padding-left 0 : padding-right 0}]) Клиентская часть Клиентскую часть я писал не совсем на clojure, а на clojurescript, который в итоге компилируется в javascript. Как фреймворк я использовал reagent — биндинг к react.js для clojure, непроверяющий каждую секунду объекты на изменения (благодаря atom’ам) и использующий hiccup-подобный dsl для описания компонентов: (defn info-box «Show info box» [text] [: div.container.col-xs-12.info-box [: h2 text]]) Тут всё очень даже хорошо, пока не нужно напрямую работать с js-библиотеками. Например, код для подключения typeahead к полю поиска: (defn init-autocomplete «Initiale autocomplete» [query langs sources] (let [input ($ »#search-input»)] (.typeahead input (js-obj «highlight» true) (js-obj «source» (fn [query cb] (cb (apply array (take const/autocomplete-limit (map #(js-obj «value» %) (get-completion query @langs @sources)))))))) (.on input «typeahead: closed» (fn [] (reset! query (.val input)))))) И даже размер «скомпилированного» файла оказался не таким уж большим — всего 290 кб.Как огромный плюс использования clojure вместе с clojurescript — можно писать один код для клиента и сервера при помощи cljx.

Выводы Хоть clojure и позволяет разрабатывать web-приложения без знания и использования html, css и javascript, но продакшен-проекты я бы так делать не решился.Исходный код результата.Сам результат.

© Habrahabr.ru