Java — суровый язык для суровых программистов.
Нет, Java-программисты не вытирают попу наждачной бумагой (хотя, это как посмотреть), но считают даже легкий намек на динамичность языка недопустимой роскошью, поэтому суровые Java-хакеры написали второй компилятор для придания своим программам малой толики функциональной комбинаторики. Саму идею ради которой собственно и был написан этот второй компилятор назвали чуть ли не новой парадигмой программирования (аспектно-ориентированное программирование, АОП) — со своей, принципиально новой терминологией, куда уж без неё.
Но это не более чем мой скромный взгляд на жизнь Java-сообщества и проект десятилетней давности «AspectJ». Определенно, создатель AspectJ, Грегор Кичалес (Gregor Kiczales), совсем не дурак — профессор Computer Science и ко всему большой лиспер, приложивший руку к Common Lisp Object System, автор книги «Art of the Metaobject Protocol», о которой небезызвестный Алан Кэй (Alan Kay) сказал
«Art of the Metaobject Protocol» это лучшая компьютерная книга написанная за последние десять лет [1990-2000].
Думаю, биография создателя AspectJ делает очевидным предположение о том, что само АОП по своей сути является попыткой перенести фичи Common Lisp в Java, на что я приведу сомнительное (citation needed) высказывание Питера Норвига
В Лиспе, если вы хотите аспектно-ориентированного программирования — нужно лишь написать немного макросов, и готово.
В Java — нужен Грегор Кичалес, создающий новую фирму, и месяцы и годы попыток заставить всё это работать.
и воспоминание Кенни Тилтона
Я помню как Грегор Кичалес на ILC 2003 [Международная конференция по Лиспу], рассказывая об AspectJ безмолвной толпе, остановился, затем жалобно добавил «Когда я показал это Java-программистам они встали и заапплодировали».
Лисп всегда был практичным языком. По крайней мере мне он всегда нравился именно за свою практичность. И если кто-то скажет, мол «лисп — академическая игрушка» — не верьте, ибо это чушь и сказка. Три больших лиспа последней четверти века: Emacs Lisp, Common Lisp и Clojure были заточены исключительно под практические нужды.
Соответственно, в старых лиспах уже очень, очень долгое время (десятки лет, насколько я могу судить) был собственный и очень добротный инструментарий для «АОП» (в кавычках — тогда это называлось по-другому): advice в Emacs Lisp и Common Lisp Object System Method Combinations (комбинаторы методов или сочетания методов… проблемы с переводом терминов).
До недавнего времени я не видел ничего подобного для Clojure, но мне хотелось испробовать АОП-шные фичи, и вот, летом 2010 года, technomancy опубликовал на гитхабе библиотеку «ловушек» Robert Hooke. Находясь под впечатлением от книги «Идеальный код», на следующий же день я её форкнул дабы привести в более кложурообразный вид и сделать чуть погибче.
С того момента когда я прочел приведенную выше цитату Норвига меня мучил вопрос — «а правда ли, что для реализации основы АОП в лиспе нужны макросы?» Я разобрался как работают ловушки technomancy, и оказалось, что макросы не нужны. Для их реализации достаточно лишь динамических переменных, метаданных и функций высшего порядка. И все это в семидесяти (!!!) строках кода. После переписывания реализации technomancy я искренне удивился компактности кода.
Ловушки по сути являются крохотным фреймворком вокруг простого, но сурового комбинатора функций — он позволяет мощным чисто функциональным механизмам сослужить нам полезную императивную службу. Technomancy в интервью назвал код этого комбинатора (2 строчки на Clojure) «своим любимым кусочком кода».
Ловушки используются для изменения поведения функции без изменения её оригинальной реализации. Это такая своеобразная техника подключения «функций-плагинов» к функции.
Функция add-hook устанавливает ловушку в одном из трех мест:
- before
- Ловушка вызывается перед вызовом оригинальной функции.
- after
- Ловушка вызывается после вызова оригинальной функции.
В before и after-ловушки имеет смысл устанавливать чисто императивные функции — только такие смогут повлиять на ход выполнения программы. При вызове в эти ловушки передаются аргументы вызова. Эти ловушки вызываются непосредственно до и после вызова оригинальной функции; все around-ловушки «оборачивают» before и after-ловушки.
- around
- Ловушка вызывается вместо вызова оригинальной функции. При вызове в неё передается функция которую ловушка «оборачивает» и аргументы вызова.
В around-ловушки можно устанавливать чистые функции, грязные функции, использующие всевозможные with-макросы, заменяющие аргументы, модифицирующие их количество — настоящее раздолье для программерской фантазии.
Функция add-hook принимает 4 аргумента, в стиле функции add-watch из clojure.core:
-
Ключ
:before
,:after
или:around
. -
Функцию, анонимную функцию или имя переменной, содержащей функцию
(например,
#'+
), около которой мы ставим ловушку. - Уникальный ключ именующий ловушку, чтобы можно было её удалить.
- Функцию которую мы ставим в ловушку.
Функция remove-hook снимает ловушку. Она принимает 3 аргумента:
-
Ключ
:before
,:after
или:around
. -
Функцию, анонимную функцию или имя переменной, содержащей функцию
(например,
#'+
), около которой мы поставили ловушку. - Уникальный ключ именующий ловушку.
Небольшой пример иллюстрирующий работу с ловушками.
(defn examine [x] (print x)) (defn microscope [f x] (f (.toUpperCase x))) (defn doubler [f & xs] (apply f xs) (apply f xs)) (defn telescope [f x] (f (apply str (interpose " " x)))) (defn into-string [f & xs] (with-out-str (apply f xs))) (add-hook :around #'examine :microscope microscope) (add-hook :around #'examine :doubler doubler) (add-hook :around #'examine :telescope telescope) (add-hook :around #'examine :into-str into-string) (add-hook :after #'examine :dotspace (fn [& args] (print \. \space))) (examine "Before i forget") > B E F O R E I F O R G E T. B E F O R E I F O R G E T. (remove-hook :after #'examine :dotspace) (remove-hook :around #'examine :doubler) (examine "Before i forget") > B E F O R E I F O R G E T (remove-hook :around #'examine :microscope) (examine "Before i forget") > B e f o r e i f o r g e t (remove-hook :around #'examine :telescope) (examine "Before i forget") > Before i forget
Вся эта магия достигается благодаря метаданным. Когда мы добавляем функцию-ловушку к оригинальной функции — они обе переезжают в метаданные третьей функции которая будет их вызывать, и эта третья занимает имя оригинальной функции. Все последующие ловушки добавляются уже непосредственно в метаданные, а когда все ловушки снимаются — все возвращается на свои места, как будто ничего и не было.
Чтобы использовать ловушки в своих программах укажите в зависимостях
Leiningen [hooks "1.0.0"]
.