суббота, 26 мая 2012 г.

Впечатления о ClojureScript

На прошлой неделе решил наконец ознакомиться с ClojureScript по-подробнее. До этого я смотрел презентацию Рича Хикки и некоторые другие скринкасты, успел "пощупать" ClojureScriptOne и почитать пару статей о том, как он (ClojureScript) крут. В общем, никаких особых сложностей с ним я не ожидал, поэтому начавшиеся приключения стали для меня откровенным сюрпризом.

Во-первых, мне не удалось создать browser-connected repl, соединенный со своей html-иной. Я выполнял простые туториалы шаг за шагом, и на этапе соединения у меня ничего не получалось. Тогда я запустил проект samples/repl, входящий в поставку ClojureScript. И все было хорошо до тех пор, пока я не попытался соединиться с веб-страницей. В результате, при помощи Дэвида Нолена и еще нескольких разработчиков ClojureScript выяснилось, что документация чуть-чуть не соответствует. После моих гневных отзывов её (документацию) наконец поправили, а я напишу здесь русский вариант туториала о том, как начать работать с ClojureScript.

1. Качаем с гитхаба последнюю версию ClojureScript.
2. Настраиваем emacs:
  a) Создаем файл browser-repl.sh с правами на выполнение:
#!/bin/sh

if [ "$CLOJURESCRIPT_HOME" = "" ]; then
  CLOJURESCRIPT_HOME="`dirname $0`/.."
fi

CLJSC_CP=''
for next in lib/: lib/*: src/clj: src/cljs: test/cljs; do
  CLJSC_CP=$CLJSC_CP$CLOJURESCRIPT_HOME'/'$next
done

java -server -cp $CLJSC_CP clojure.main -e \
"(require '[cljs.repl :as repl])
(require '[cljs.repl.browser :as browser])
(def env (browser/repl-env))
(repl/repl env)"

б) Прописываем в .emacs настройки для inferior-lisp'a:
(setq inferior-lisp-program "~/clojure/clojurescript/browser-repl.sh")

Здесь "~/clojure/clojurescript/browser-repl.sh" -- естственно, путь к созданному на шаге 2а файлу browser-repl.sh.

3. Добавляем clojurescript/bin в $PATH.

4. Заходим в каталог clojurescript/samples/repl, компилируем исходники: cljsc src/ > main.js

Файл main.js будет позднее использоваться в index.html

Обратите внимание на содержимое файла test.cljs. Он содержит такую строчку:
(repl/connect "http://localhost:9000/repl")
Именно она позволяет затем соединиться с Emacs-ом.

5. Файлы index.html, main.js и каталог out необходимо захостить на каком-нибудь веб-сервере. Я, например, просто создал каталог Tomcat7/webapps/my и сложил туда все это барахло. В моем случае страница index.html стала доступна по адресу localhost:8080/my/index.html.

Внимание! Хостить страницу обяхательно, просто открыв её как файл с диска вы  не сможете к ней подконнектиться из Emacs-а!

6. Запускаем Emacs, выполняем команду M-x inferior-lisp.

7. Вводим следующие команды:
(require '[cljs.repl :as repl])
(require '[cljs.repl.browser :as browser])
(def env (browser/repl-env))
(repl/repl env)


8. Загружаем/обновляем index.html в броузере. После этого repl в Emacs-е должен заработать. Например, вычисление (+ 1 1) должно выдать ответ 2. Если это произошло -- вуаля! у вас веб-консоль прямо в Emacs-е!


В моем примере index.html содержит следующий фрагмент:

      <div id="dates">
 <div>23.05.2012</div>
 <div>25.05.2012</div>
 <div>27.05.2012</div>
 <div>28.05.2012</div>
 <div>29.05.2012</div>
      </div>
 
Таким образом, команды на скриншоте изменяют размер шрифта и цвет текста первой даты.

Вообще, документация на ClojureScript достаточно бедная. Поэтому, вместо того, чтобы искать блог-посты и туториалы по использованию ClojureScript, я просто стал читать его исходники. Рекомендую для этого воспользоваться инструментом Marginalia. С Leiningen 2 это вообще просто.

1. В .lein/profiles.clj добавьте плагин lein-marginalia

{:user {:plugins [ [lein-marginalia "0.7.0"]
                   [lein-pprint "1.1.1"]
                   [lein-swank "1.4.4"]]}}

2. Создайте новый проект: lein new clj-docs
3. Скопируйте в катлог cljs-docs/src содержимое каталогоа cljorescript/src.
4. Выполните команду lein marg, и в каталоге docs будет создан файл uberdoc.html, который слева содержит комментарии к исходнику,  а справа -- сам исходник.


Еще стоит ознакомиться с Google Closure API -- тоже полезное подспорье в работе с ClojureScript.

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


вторник, 8 мая 2012 г.

Гусеничный Lisp-трактор

Просматривая записи Казимира Майорника, нашел у него очень любопытную статью. Это всего лишь забавный эксперимент, показывающий, как при помощи лисповой парадигмы "код-как-данные" можно реализовать циклическое выполнение кода без циклов и рекурсий. Никакой практической ценности в этом нет, просто красиво смотрится. Я написал что-то похожее на Clojure, но несколько усовершенствованное. В моей версии алгоритм выглядит так.

1) Список f содержит всего одну функцию. Её и запускаем первой.
2) Первая функция передает свой код в update.
3) Update "препарирует" код, находит номер текущей итерации, инкрементирует его и собирает такую же функцию, как ей передали, но с новым значением итерации.
4) Первая функция получает вторую функцию и добавляет её в конец списка f. Если список f содержит более трех элементов, то первый элемент списка f удаляется. Таким образом, в списке f содержится не более четырех элементов.
5) Первая функция запускает вторую функцию и завершается.

Выглядит и впрямь, как гусеничный трак :-)

(defn update [func]
  (let [p (nth func 2)        ;; Получить список (println (str "Hi for the " 1 " time!"))
        n (nth (second p) 2)  ;; Получить 1 из списка
        ;; Создать список (println (str "Hi for the " 2 " time!"))
        new-p (cons 'println (list (map #(if (= n %) (inc n) (identity %)) (second p))))]
    ;; Заменить в теле функции (println (str "Hi for the " 1 " time!"))
    ;; на (println (str "Hi for the " 2 " time!"))
    (map #(if (and (seq? %) (= (first %) 'println)) new-p (identity %)) func)))

(def f (ref ['(fn []
                (println (str "Hi for the " 1 " time!"))
                (let [func (last @f)
                      ;; Обновить список функций f
                      ;; Удалить первую функцию, если список f содержит больше трех элементов
                      new-f (if (> (count @f) 3)
                              ;; Добавить обновленную функцию в конец списка
                              (conj (vec (drop 1 @f)) (update func)) 
                              (conj @f (update func)))]
                  (dosync (ref-set f new-f))
                  ;; Выполнить последнюю функцию списка
                  ((eval (last @f)))))]))

((eval (last @f)))

понедельник, 7 мая 2012 г.

CEDET 1.1, Emacs и Java


Недавно вышедший Cedet 1.1 добавляет долгожданную поддержку импорта символов из jar-ок. Теперь Emacs предоставляет поддержку автодополнения кода java примерно на том же уровне, что и современные IDE.
Например, у нас есть класс My1:


Он используется в My2:


Обратите внимание, как flymake параллельно скомпилировал файл и подсветил ошибки. Если набрать “my.” и выполнить функцию semantic-ia-complete-symbol-menu (у меня она замаплена на Ctrl-Space), то Emacs выдаст такую подсказку:



Аналогично можно запрашивать подсказку по членам системных классов. Например, для StringBuilder:


Правда надо учитывать, что, во-первых, импорты “звёздочкой” не срабатывают, системных классов semanticdb-javap тоже не знает. Поэтому, если необходимы подсказки, то импорт классов из неймспейса java.lang нужно делать явно, как в моем примере.

Для того, чтобы заработали показанные выше механизмы, необходимо следующее:
  1. установленный пакет flymake;
  2. установленный пакет auto-complete;
  3. скачанный CEDET 1.1;
  4. Ну и конечно же, последний Emacs и java.

Конфигурация CEDET в .emacs :
(load-file "~/cedet/common/cedet.el")             ;; Подгрузить cedet
(global-semanticdb-minor-mode 1)                  ;; Инициализировать semanticdb
;; Загрузить поддержку авто-дополнения
(semantic-load-enable-gaudy-code-helpers)

(custom-set-variables             ;; Инициализация переменных, указывающих, где 
                                                      ;; установлена java
. . .
'(cedet-java-jdk-root "/opt/jdk")

 ;; Сюда нужно добавить все jar-ки, из
 ;; которых необходимо импортировать символы
'(semanticdb-javap-classpath '("/opt/jdk/jre/lib/rt.jar"))
. . .
)

Конфигурация flymake:
(require 'flymake)
(add-hook 'java-mode-hook 'flymake-mode-on)

(defun my-java-flymake-init ()
(list "javac" (list (flymake-init-create-temp-buffer-copy
'flymake-create-temp-with-folder-structure))))

(add-to-list 'flymake-allowed-file-name-masks '("\\.java$" my-java-flymake-init flymake-simple-cleanup))

Конфигурация auto-complete:
(require 'auto-complete-config)
(add-to-list 'ac-dictionary-directories "~/.emacs.d/ac-dict")
(ac-config-default)



четверг, 29 марта 2012 г.

Итоги за месяц

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

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

Проект, который я делал с начала февраля по середину марта, -- андроид-приложение для планшетов. Оно предназначалось для дистанционного обучения какой-то фигне сотрудников какой-то компании. Технических сложностей там никаких нету, писать здесь не о чем, за исключением достаточного большого объема работы: необходимо было реализовать 22 формы со всякими разными украшениями. Дополнительная сложность -- необходимо написать еще и JSON веб-сервис, который может отдавать запрашиваемые данные и сохранять у себя ответы пользователей на опросы и тесты.

Дедлайн был очень жесткий: 12 марта. На мой взгляд, при таких объемах и сроках проект должен делать не один человек, а как минимум трое.

Во-первых, мне удалось уговорить заказчика разрешить писать веб-сервис на Clojure вместо java. По моим прикидкам, это сэкономило бы мне неделю работы. Для заказчика были крайне важны сроки, и он позитивно воспринял мое предложение, но поинтересовался: какие минусы у clojure? Я честно сказал, что этот язык просто менее распространен, чем java. Все остальное -- то же, что и у java. Например, готовый сервис мы захостим под Tomcat в виде war-файла. Заказчик согласился с этим риском, заметив, что если вдальнейшем будет необходимость, то тогда вебсервис можно будет переделать на java. Забегая вперед скажу, что необходимость не возникла :-)

Я воспользовался библиотеками noir1.3-alpha10, cheshire, sqlkorma, lobos и очень быстро показал работающий сервак. Функциональность в сервак я добавлял в процессе работы над клиентской частью по мере необходимости. Поскольку я пользовался связкой Emacs+Slime, рабочий код я получал очень быстро. А время, которое я тратил на разработку очередной функции сервака, в среднем равнялось нескольким минутам.

Основное время, конечно же, занимала Android-часть, причем в основном -- настройка внешнего вида формочек. Android -- неплохая платформа для пользователя, но очень сырая и недоделанная для разработчика. В отличие от ябловодов, нам, андроидам, нет возможности пользоваться WYSIWYG-редактором для построения GUI. А редактировать гигантские простыни xml-кода вручную -- сомнительное удовольствие...

Решение нашлось довольно быстро. Я разработал небольшой DSL на основе Clojure и Hiccup. С применением этого DSL формочку я целиком описывал на Clojure, а потом генерил из неё связку xml и java-кода (при этом генерился весь MVC на форму). В среднем, количество кода в DSL и количесство сгенеренного xml+java кода соотносились как 1:3 или 1:4, что уже очень неплохо. Еще один важный бонус, который я получил от этого DSL -- возможность использования нормального вменяемого языка для описания UI вместо XML. Появилась возможность выносить общую функциональность в функции, делать циклы и т.д. Понятно, что большинство повторно используемых компонент оформлялись как отдельные виджеты; но там, где для этого не было необходимости -- я просто объявлял функцию на Clojure. Еще важное замечание: код в стиле Hiccup читается гораздо проще, чем xml.

Дальше развивать свой DSL у меня уже не было времени, а повторяющийся код, который я не мог обобщить, все равно встречался. Например, код для работы с БД и вебсервисом состоял по большей части из извлечения-записи в хеш-таблицы по ключам, соответствующим именам полей в БД. Здесь мне на помощь пришел Emacs. Буквально пара десятков строк кода на elisp-е, и весь оставшийся boilerplate-код на java генерил Emacs.

Надо сказать, что в сроки я уложился, но с учетом, что в конце проекта на 2 недели мне дали в помощь товарища. Думаю, получилось очень даже хорошо.

воскресенье, 19 февраля 2012 г.

Деплоймент Clojure под Tomcat

Не так давно я взялся за небольшой проект с клиент-серверной архитектурой. JSON веб-сервис мне позволили реализовать на clojure, что я очень быстро и сделал при помощи библиотек: Noir, Lobos, Korma и Cheshire. Но зато была масса непоняток с деплойментом этого готового сервиса на хостинг. Во-первых, по непонятной причине, оригинальный сервер, поработав какое-то время, попросту отваливается. Я до сих теряюсь в догадках, что заставляет его так капризничать. Понятное дело, что я решил задеплоить всё на Tomcat.

Мне не удалось нигде найти нормальной инструкции о том, как деплоить noir-приложение под томкат, так что я решил восполнить этот пробел.

Данная инструкция рассчитана на веб-сайт, разработанный на clojure при помощи фреймворка Noir (опробовано на noir 1.3.0-alpha10).

1) Добавьте [lein-ring "0.5.4"] в :dev-dependencies вашего файла project.clj.
Внимание! [uk.org.alienscience/leiningen-war "0.0.13"] – это не то же самое, что lein-ring. Плагин lein-ring умеет создавать ring-handler для noir-приложения и запаковать все в war-ку. А плагин leiningen-war только создает war-ку, а ring-handler-а не создает.

2) В файле project.clj укажите ваш ring-handler и пространство имен для ahead-of-time компиляции.
:ring {:handler MyProject.server/handler}
:aot [MyProject.server]

3) Создайте ring-handler в server.clj:
(:require [noir.server :as server])
. . .
(def handler (server/gen-handler {:base-url "/myproject"}))
Внимание! Обязательно нужно указать :base-url, иначе редиректы и линки задеплоенного в томкат приложения будут указывать не на localhost:8080/myproject/, а на localhost:8080/ . Эпик фейл будет фееричный!

4) Уберите такую загрузку вьюх: (server/load-views "src/MyProject/views/") из файла server.clj. Вьюхи нужно загружать явно:  
(:require [noir.server :as server]
. . .
            [MyProject.views.pages])
Если этого не сделать, то после упаковки в war-ку и даже jar-ку, ни один ваш маршрут работать не будет.

5) Все линки и формы, если вы их генерите из hiccup, нужно создавать функциями:
(hiccup.form-helpers/form-to [:post "/myurl"] ...)
(hiccup.page-helpers/link-to "/myurl" …)
Если этого не сделать, то они будут вести на localhost:8080/ вместо localhost:8080/myproject.

6) Создайте war-ку командой lein ring uberwar.
Внимание! Команда lein ring uberwar поставляется с lein-ring-плагином, и в нашем случае она работает. Команда lein uberwar поставляется с плагином  leiningen-war, и здесь она не работает. Так что этот плагин вообще лучше не использовать с noir.

7) Скопируйте war-ку в webapps томката и переименуйте её в myproject.war.

Теперь, после запуска томката, по адреусу localhost:8080/myproject будет ваше приложение.

воскресенье, 22 января 2012 г.

Noir benchmark

Решил немного потестить простое веб-приложение, описанное в предыдущей статье. Тесты проводились на ноутбуке Acer Aspire 5745G (Intel Core i5 2.26Ghz, 4Gb). Обнаружил следующее.

Во-первых, при запуске сайта командой lein run запускаются два java-процесса. Первый, занимающий 28.8Mb -- видимо служебный, т.к. никакие его характерстики за время работы приложения не менялись. Второй, сходу запросивший 76.7Mb, -- само веб-приложение. Я создал два типа сущностей: mypage и mypage2 с CRUD-действиями для них, не используя БД. Т.е. скорость обработки запроса целиком зависила от скорости работы java-машины и не зависела от скорости ввода-вывода или поиска данных в таблицах. При обращении к различным действиям сущности mypage объем потребляемой памяти возрос до 90.3Mb. При обращении к mypage2 -- до 93.5Mb. При повторном обращении к этим же сущностям потребление памяти никак не поменялось. При перезапуске приложения цифры изменились до 96Mb и 99Mb.

Интересно взглянуть на производительность веб-приложения под нагрузкой. Я создал скрипт, делающий 70000 таких запросов:
wget http://localhost:8080/mypage
wget http://localhost:8080/mypage/848867
wget http://localhost:8080/mypage/edit/996283
wget http://localhost:8080/mypage/527997
wget http://localhost:8080/mypage/edit/410389

Сущности с указанными айдишниками, естественно, существовали в приложении. Эти 70тыс. запросов jetty обработал за полчаса, отдавая, таким образом, примерно 2333 динамических страницы в минуту или примерно 39 в секунду.

Самым интересным для меня в этом эксперименте стало потребление памяти и процессорного времени.

Сначала jetty затребовал 92% процессорного времени, но его запросы довольно быстро сократились до 40%. Затем, довольно медленно, и они стали снижаться. К концу эксперимента jetty потреблял всего 4% процессорного времени.

Аналогично, запросы памяти сначала были немалыми. Довольно быстро jetty "откушал" 232.8Mb, после чего потребляемая память стала уверенно снижаться, остановившись на отметке 84.6Mb.

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

Такое снижение потребляемой памяти и процессора стало возможно благодаря run-time оптимизациям java-машины (в Sun некогда здорово над этим поработали). Вот и скажи теперь, что java -- медленная...

воскресенье, 8 января 2012 г.

CRUD на Clojure средствами Noir


Давно собирался рассказать немного о Noir – классном веб-фреймворке для Clojure. Наконец, у меня появилась возможность написать большой туториал по применению Noir, что я с удовольствием и сделаю :-)

Есть ли вообще необходимость в самоучителе по Noir? Не очень существенная, если честно. На сайте фреймворка (webnoir.org) более чем приличная документация: раздел для быстрого старта, несколько обучалок по использованию Noir и даже индекс API. Мой туториал, возможно, будет вам полезен, если вы, зная Clojure и разобравшись более-менее с Noir, все еще не очень представляете себе архитектуру своего будущего приложения.

А задуматься над архитектурой придется. Noir очень сильно отличается от всех известных мне фреймворков. При своем минимализме он оставляет выбор конкретных библиотек за пользователем – такая свобода может привести к неверному их использованию. Насколько этот выбор широк можете судить хотя бы по статье с обзором разных компонентов для каждого из уровней веб-стека на clojure.

Давайте посмотрим, на что похоже приложение Noir. Если вы проследовали инструкциям из Getting Started на сайте webnoir.org, то у вас уже установлен leiningen-плагин для Noir. Создадим новый проект командой lein noir new optimalist. У него очень простая структура.
resources – содержит каталог public со статическим контентом (css, javasript, images);
src – каталог с исходниками;
test – тесты;
project.clj – описание проекта и версий подключаемых библиотек.

Исходники содержаться в двух каталогах src/optimalist/views и src/optimalist/models. Никаких вопросов не вызывает? А как насчет src/optimalist/controllers? Такого каталога нету, т. к. самописных контроллеров здесь не предусмотрено. Это, конечно, у многих может вызвать разрыв шаблона: как вообще программировать MVC без C!

Автор Noir Крис Гранжер очень подробно обосновал свое представление архитектуры. Исторически сложилось, что бизнес-логику обычно помещали в контроллер. Но со временем её все больше и больше выносили в отдельные компоненты, контроллер стал совсем тоненьким и, откровенно говоря, ненужным. Функции маршрутизации берет на себя фреймворк, так что в Noir (да и в других современных веб-фреймворках) строгой необходимости в отдельных контроллерах попросту нету. Единственное, что всё еще делает контроллер – это указывает, какую страницу рендерить в ответ на запрос пользователя. Раз уж код контроллера такой маленький, то Крис Гранжер попросту решил перенести функции контроллера на уровень View.

Если сравнивать такой подход с другими фреймворками, то, пожалуй, Крис – один из первых, кто открыто решился заявить о ненужности контроллеров. Например, в практике программирования Ruby on Rails всё еще считается правильным всю бизнес логику вынести на уровень модели, а контроллеры сделать маленькими и легковесными. Это очень упрощает тестирование. Но это половинчатый подход: выбросили бы контроллеры да и дело с концом! Скажем, в JSF2.0 примерно так и обстоят дела. Вся бизнес-логика находится в Java-бинах, вызываются они с уровня View напрямую, минуя слой контроллера. Маршруты привязываются к страницам обычно централизованно в специальном файле конфигурации.

Поскольку каталог models пустой, давайте взглянем повнимательнее на содержимое каталога views.

В файле common.clj создается специальная функция layout. Она задает общий шаблон страницы, заголовки и структуру html-файла.

В файле welcome.clj при помощи defpage задается маршрут «/welcome» и функция, которая сгенерирует ответ на запрос пользователя по этому маршруту. Как видно из кода, функция вернет строку приветствия. Шаблон html-страницы создается при помощи библиотеки Hiccup. Она позволяет писать html-код прямо на языке clojure при помощи родных для clojure структур данных: векторов и мапов.

Hiccup очень удобен тем, что позволяет производить любые манипуляции с шаблоном прямо при помощи Clojure. Заметьте, такой подход вообще не встречается среди мейнстримных фреймворков. Что в ASP.NET, что в J2EE для разметки страницы применяется свой собственный DSL, построенный на базе XHTML. Как, например, сделать повторяющийся фрагмент страницы на JSF2.0, на которой еще есть экшены? Правильно, с применением ломика и какой-то матери. В ASP.NET с этим, вроде, несколько проще, но подход такой же: xml-теги. Ну и скажите мне, как программисту Java, почему я не могу использовать Java для изменения шаблона веб-страницы? Как ни крути, но даже Java... хотя нет, уж лучше этот долбаный XML, чем веб-страницы на Java...

Короче, в Noir используется hiccup, а hiccup позволяет писать html-страницы на clojure в декларативном стиле.

Вручную hiccup-код имеет смысл писать только для страниц очень небольшого размера. Если вам досталась от дизайнера монстрозная страница в десятки килобайт кода – конечно, такое поделие переписывать на hiccup не стоит. В этом вам поможет clj-tagsoup. Просто вызовите его функцию parse с указанием html-страницы и сохраните полученный hiccup-код.

Почти все руководства о веб-фреймворках в виде первого учебного приложения предлагают свой вариант блога. Давайте и мы сделаем свой простой блог при помощи Noir. Переименуем welcome.clj в article_pages.clj (а также namespace) и напишем код первой страницы блога – списка записей. Еще я немного изменил список импортируемых библиотек.
(ns optimalist.views.article_pages
  (:require [optimalist.views.common :as common]
            [noir.response :as resp])
  (:use [noir.core]
        [hiccup.core]))

(defpage articles "/articles" []
         (common/layout
           [:p "List of articles"]))
Обратите внимание, что маршрут «/articles» – именованный. В будущем я смогу получить его при помощи (url-for articles).

Запустите веб-приложение командой lein run и откройте его по ссылке localhost:8080/articles. Разумеется, вы увидите текст «List of articles». Но пока что еще не работает корневой маршрут: localhost:8080/. Давайте добавим для него определение маршрута.
(defpage "/" []
  (resp/redirect (url-for articles)))
Как видите, здесь объявляется неименованный маршрут «/», и вместо ответа на него сервер редиректит на страницу articles.

Следующая проблема: как вывести список каких-либо элементов? Поскольку код шаблона мы пишем на clojure, то воспользуемся функцией for:
(defpage articles "/articles" []
  (let [data [{:id 1, :title "First title", :body "First body"}
              {:id 2, :title "Second title", :body "Second body"}
              {:id 3, :title "Third title", :body "Third body"}]]
         (common/layout
          [:p "List of articles" [:br] [:br]
           (for [item data]
             [:div
              (:title item)
              [:br]
              (:body item)
              [:br][:br]])])))

Пока что все достаточно просто, не так ли? Страница articles покажет нам следующий текст:

List of articles
First title
First body

Second title
Second body

Third title
Third body

Теперь данные нужно перенести в отдельный файл. Создадим в каталоге models файл article_model.clj, добавим туда код:
(ns optimalist.models.article_model)

(def *data*
  (ref
   [{:id "1", :title "First article", :body "Lorem ipsum dolor sit amet."}
   {:id "2", :title "Second article", :body "Lorem ipsum dolor sit amet."}
   {:id "3", :title "Third article", :body "Lorem ipsum dolor sit amet. "}
   {:id "4", :title "Fourth article", :body "Lorem ipsum dolor sit amet."}]))

(defn fetch-list []
  @*data*)
Воспользуемся этой моделью. Чтобы импортировать модель, добавим в секцию require такую строку: [optimalist.models.article_model :as article-model]; а код функции articles перепишем таким образом:
(defpage articles "/articles" []
  (common/layout
   [:p "List of articles" [:br] [:br]
    (for [item (article-model/fetch-list)]
      [:div
       (:title item)
       [:br]
       (:body item)
       [:br][:br]])]))
Вообще такая организация кода всё еще не очень удобная. Что если будет несколько страниц с похожим функционалом? Нам придется выносить hiccup-код в отдельные модули чтобы сделать его reusable. Давайте сделаем это сразу. Создатим файл view/article_templates.clj и вынесем туда генерацию httml.
(ns optimalist.views.article_templates
  (:use [noir.core]
        [hiccup.core]))

(defn show-list [articles-list]
   [:p "List of articles" [:br] [:br]
    (for [item articles-list]
      [:div
       (:title item)
       [:br]
       (:body item)
       [:br][:br]])])
В модуле article_pages.clj необходимо добавить строку [optimalist.views.article_templates :as article-view] в секцию :require. А страница articles тогда будет выглядеть так:
(defpage articles "/articles" []
  (common/layout
   (article-view/show-list (article-model/fetch-list))))
Следующая наша задача – добавить к элементам списка ссылки на действия. Лично я – большой любитель технологии REST, о которой узнал из Ruby on Rails. Наши четыре действия над сущностью create, read, update, delete подразумевают наличие семи маршрутов:

Маршрут Метод Описание
/articles GET Список записей
/article/:id GET Просмотреть одну запись
/article/edit/:id GET Показать форму редактирования записи
/article/update/:id POST Изменить запись в соответствии с указанными данными
/article/delete/:id POST Удалить запись
/article/new GET Показать пустую форму для создания записи
/article/create
POST
Создать запись в соответствии с указанными данными

Вообще в REST должны использоваться методы не только GET и POST, а также PUT (для создания новой записи) и DELETE (для удаления). Но то ли браузеры не понимают этих методов, то ли веб сервера, но факт остается фактом: использовать мы можем только GET и POST. В Ruby on Rails для обхода этого ограничения создается скрытое поле. И если указывается метод PUT или DELETE, то на самом деле используется POST с пометкой в скрытом поле, что это все-таки PUT/DELETE.

Разные методы отправки запроса нужно использовать не только для красоты. По сети ползают роботы-индексаторы, которые пытаются перейти по ссылкам на другие страницы. Представьте, например, что на вашем сайте кнопка DELETE сделана при помощи обыкновенной ссылки (запрос методом GET), и робот решит по ней перейти. Робот ведь не знает, что эта ссылка приводит к какому-то разрушительному действию, правда? Вот для этого-то все запросы, как-нибудь изменяющие сущность, должны быть отправлены методом POST.

Итак, первая ссылка, которую я хотел бы добавить к списку сущностей – маршрут /article/:id для просмотра записи.

Вначале в article_pages добавим страницу:
(defpage view-article "/article/:id" {id :id}
  (common/layout
   "View article"))

Она пока ничего не делает, но зато её сразу же можно протестить в браузере по маршруту /article/1. Заметьте, что в определении страницы задан хеш параметров {id :id}. Это деструктуризаця параметров запроса, и делает она именно то, на что похоже: создает локальную переменную id, значение которой будет получено из части маршрута :id.

Теперь добавим ссылку на эту страницу в функции show-list
(defn show-list [articles-list]
   [:p "List of articles" [:br] [:br]
    (for [item articles-list]
      [:div  (:title item) [:br]
       (:body item) [:br]
       [:a {:href (url-for (str "/article/" (:id item)))} "View"]
       [:br][:br]])])
Функция url-for умеет создавать абсолютный URL по заданным параметрам. Параметром может быть как текстовая строка с маршрутом, так и сам именованный маршрут. И хотя предложенное выше решение – вполне работоспособное, но оно все-таки не оптимальное. Что если вы решите изменить URL в странице view-article? Тогда вам придется искать все места, где вы на него ссылаетесь, и править маршруты вручную. Лучше использовать именованый маршрут следующим образом:

(url-for optimalist.views.article_pages/view-article {:id (:id item)})

Здесь optimalist.views.article_pages/view-article – полное имя именованного маршрута. Необходимо писать его целиком, т. к. в файле article_templates.clj ничего не известно о aricle_pages.clj. И импортировать article_pages.clj мы тоже не можем, т. к. там уже есть импорт article_templates.clj, и мы получим циклическую зависимость. Поэтому, все-таки, придется писать целиком.

Второй параметр – хеш значений, которые будут переданы в генератор маршрута. Поскольку наш маршрут выглядит как «/article/:id», то ему нужно передать хеш {:id <значение>}.

Вообще, конечно же, такая запись жутко громоздкая и неудобная. Поэтому в начало файла article_templates.clj, после объявления неймспейса, добавим алиас:

(alias 'article 'optimalist.views.article_pages)

Теперь генерацию ссылки можно переписать таким образом:

(url-for article/view-article {:id (:id item)})

Это уже значительно лучше, но все еще пока не идеально. Элементов типа «ссылка» у нас будет достаточно много, особенно, если много разных сущностей. Но типов ссылок всего несколько: new, view, edit, delete, back. Мы могли бы сделать более абстрактный механизм генерации ссылок, который инкапсулирует в себе всю внутреннюю структуру вектора [:a …], в том числе и надпись на ссылке. Создадим файл views/helpers/widget.clj: 
(ns optimalist.views.helpers.widget
  (:require [noir.core]))

(defmacro view-link [page id]
  `(vec [:a
     {:href (noir.core/url-for ~page {:id ~id})} "View"]))
Теперь его можно подключить в article_templates.clj, добавив строку [optimalist.views.helpers.widget] в секцию :use. А вместо записи [:a {href … } «View»] вставим вызов макроса: (view-link article/view-article (:id item)).

Макрос здесь нужен из-за параметра page – символа, к которому привязан именованный маршрут. Конечно, его можно было бы передавать закавыченным, но это не очень-то красиво.

Сам макрос пока-что не слишком удачный. Что если нам понадобится добавить еще несколько похожих ссылок? Copy-paste – не наш метод, поэтому немного расширим наш язык для данного конкретного случая.

Это очень интересная технология, описанная Полом Грэмом в книге «On Lisp», и называется она «bottom-up programming». Идея заключается в том, что вы не только пишете свое приложение, используя Lisp, но и изменяете Lisp, чтобы легче было писать ваше предложение. Снизу, в язык вводятся новые конструкции, упрощающие реализацию приложения. Сверху, приложение описывается при помощи ваших новых конструкций, что делает код значительно короче. Отсюда и bottom-up programming.

Новые конструкции, которые мы вводим в язык, называются утилитами. Давайте добавим две таких:
(defmacro common-url [page id]
  `(noir.core/url-for ~page {:id ~id}))

(defmacro common-link [url text & params]
  `(vec [:a (merge {:href ~url} ~@params) ~text]))
Первый макрос просто инкапсулирует знание о том, как создавать URL. Теперь нам не надо заботиться о том, чтобы дописывать noir.core к url-for, а также создавать хеш для единственного занчения id.

Второй макрос создает html-ссылку с заданным текстом на указанный URL. Необязательный параметр params – это возможные параметры самой ссылки. Например, если нам потребуется задать обработчик onclick, то мы сможем сделать это через params.

Перепишем текст генерации ссылки с применением двух новых утилит:
(defmacro view-link [page id]
  `(common-link (common-url ~page ~id) "View"))
  
(defmacro edit-link [page id]
  `(common-link (common-url ~page ~id) "Edit"))
 Итак, на данный момент, у нас есть список записей и ссылка на просмотр каждой записи. Давайте теперь реализуем функцию view-article:
(defpage view-article "/article/:id" {id :id}
  (common/layout
   (article-view/show
    (article-model/fetch id))))
Чтобы она заработала, нужно в article_templates добавить функцию show:
(defn show [article]
  [:div
   [:h1 (:title article)]
   [:div (:body article)]
   [:br]
   [:a {:href (url-for article/articles)} "Back"]]) 
В article_model необходимо добавить функцию fetch:
(defn fetch [id]
  (first
   (filter #(= id (:id %)) @*data*)))
На этом, пожалуй, все сложности и закончились. Дальше, по накатанной. Проект целиком вы можете найти здесь, а вот как выглядят два основных файла проекта.

article_pages.clj

(ns optimalist.views.article_pages
  (:require [optimalist.views.common :as common]
            [noir.response :as resp]
            [optimalist.views.article_templates :as article-view]
            [optimalist.models.article_model :as article-model])
  (:use [noir.core]))

(defpage "/" []
  (resp/redirect (url-for articles)))

(defpage articles "/articles" []
  (common/layout
   (article-view/show-list (article-model/fetch-list))))

(defpage view-article "/article/:id" {id :id}
  (common/layout
   (article-view/show (article-model/fetch id))))

(defpage edit-article "/article/edit/:id" {id :id}
  (let [item (article-model/fetch id)]
    (common/layout
     (article-view/form (url-for update-article {:id id}) "Update" (:title item) (:body item)))))

(defpage update-article [:post "/article/update/:id"] {:keys [id title body]}
  (article-model/update id title body)
  (resp/redirect (url-for view-article {:id id})))

(defpage new-article "/article/new" []
  (common/layout
   (article-view/form (url-for create-article) "Create")))

(defpage create-article [:post "/article/create"] {:keys [title body]}
  (article-model/create title body)
  (resp/redirect (url-for articles)))

(defpage delete-article [:post "/delete/:id"] {id :id}
  (article-model/delete id)
  (resp/redirect (url-for articles)))

article_templates.clj

(ns optimalist.views.article_templates
  (:require [optimalist.views.helpers.widget :as widget])
  (:use [noir.core]
        [hiccup.core]))

(alias 'article 'optimalist.views.article_pages)

(defn show [article]
  [:div
   [:h1 (:title article)]
   [:div (:body article)]
   [:br]
   [:a {:href (url-for article/articles)} "Back"]])

(defn list-item [item]
  (let [id (:id item)]
    [:div [:div (:title item)]
     (:body item)
     [:form {:id (str "form-" id) :method "POST", :action (url-for article/delete-article {:id id})}
      [:div
       (widget/view-link article/view-article id) " | "
       (widget/edit-link article/edit-article id) " | "
       (widget/delete-link id)]]
     [:br][:br]]))

(defn show-list [articles-list]
  [:div
   (for [item articles-list] 
     (list-item item))
   [:a {:href (url-for article/new-article)} "New"]])

(defn form [url button-text & [title body & rest]]
  [:form {:method "POST", :action url}
   [:input {:name "title", :type "text", :value title}]
   [:br]
   [:textarea {:name "body"} body]
   [:br]
   [:input {:type "submit", :value button-text}]
   [:br]
   [:a {:href (url-for article/articles)} "Back"]])