
I recently met an interesting language - clojure. I immediately liked the lazy and immiable collections, stm, macros,
abundance of parentheses and dsl for all occasions.
And I decided to try to make a web application using only clojure.
application
It was conceived to create a simple search for subtitles, which:
- every 5 minutes indexes new subtitles on addicted, notabenoid and other services;
- has a one-page web-interface with search without reloading the page;
- shows in the web-interface the number of indexed subtitles and changes it when new ones appear;
- has a simple api for interacting with a desktop client .
Parsers
Surprisingly, parsers were easy and convenient to write. At first it seemed that there were too many parentheses, but the threading macros (
-> ,
- >> ,
- <> and - <>> - passing the result to the next expression) gave a lot of help.
')
For example, a piece of the notabenoid parser that does the same thing in python and 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 brackets | 14 brackets |
To run parsers, the
at-at library is used, for parsing html -
enlive . The result is recorded in elasticsearch.
Server part
Server
As a server, I chose the
http-kit , mainly due to the fact that I wanted web sockets. And they are very easy to use here, for example, sending all customers the number of indexed subtitles after the update will look like this:
(add-watch total-count :notifications #(doseq [con @subscribers] (send! con (prn-str {:total-count %4}))))
Routing
For routing -
compojure . There are no differences from django and other popular frameworks:
(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
Since we use clojure everywhere, our api should return the result in native data structures and in json (for a python desktop client). I did not find the library that can do this
(I already found it) , so I had to devote a little and invent my mini-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)))
And as a simple usage example:
(defapi list-languages "List all available languages" [params] (models/list-languages))
View
For html rendering, I used a special dsl -
hiccup , the template with it looks a bit "Martian":
(defn index-page [] (html5 [:head [:title "Subman - subtitle search service"] [:body [:h1 "Welcome to subman!"]]))
Styles
For styles, clojure also has its own dsl -
garden . The code with it looks strange too:
(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}])
Client part
I wrote the client part not entirely on clojure, but on clojurescript, which eventually compiled into javascript. As a framework, I used
reagent - binding to react.js for clojure, not checking objects for changes every second (thanks to atoms) and using a hiccup-like dsl to describe the components:
(defn info-box "Show info box" [text] [:div.container.col-xs-12.info-box [:h2 text]])
It's all very well, as long as you do not need to directly work with js-libraries. For example, the code to connect typeahead to the search field:
(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))))))
UPD: After a little refactoring, the code became less scary:
(defn completion-source "Source for typeahead autocompletion" [langs sources query cb] (cb (->> (get-completion query @langs @sources) (map #(js-obj "value" %)) (take const/autocomplete-limit) (apply array)))) (defn init-autocomplete "Initiale autocomplete" [query langs sources] (let [input ($ "#search-input")] (.typeahead input #js {:highlight true} #js {:source (partial completion-source langs sources)}) (.on input "typeahead:closed" #(reset! query (.val input)))))
And even the size of the “compiled” file was not so big - only 290kb.
As a huge plus of using clojure with clojurescript, you can write one code for a client and a server using
cljx .
findings
Although clojure allows you to develop web applications without the
knowledge and use of html, css and javascript, but I would not decide to do production projects.
Result source code.The result itself.