Я без особой на то причины просматривал исходный код на clojure.core
.
Я начал читать defmacro ns
, вот сокращенный исходник:
(defmacro ns
"...docstring..."
{:arglists '([name docstring? attr-map? references*])
:added "1.0"}
[name & references]
(let [...
; Argument processing here.
name-metadata (meta name)]
`(do
(clojure.core/in-ns '~name)
~@(when name-metadata
`((.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata)))
(with-loading-context
~@(when gen-class-call (list gen-class-call))
~@(when (and (not= name 'clojure.core) (not-any? #(= :refer-clojure (first %)) references))
`((clojure.core/refer '~'clojure.core)))
~@(map process-reference references))
(if (.equals '~name 'clojure.core)
nil
(do (dosync (commute @#'*loaded-libs* conj '~name)) nil)))))
А потом, пытаясь прочесть его, я увидел несколько странных шаблонов макросов, в частности, мы можем взглянуть на:
~@(when name-metadata
`((.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata)))
clojure.core
Вот автономное рабочее извлечение из макроса:
(let [name-metadata 'name-metadata
name 'name]
`(do
~@(when name-metadata
`((.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata)))))
=> (do (.resetMeta (clojure.lang.Namespace/find (quote name)) name-metadata))
Когда я запустил это, я не мог не задаться вопросом, почему в точке `((.resetMeta
есть двойной список.
Я обнаружил, что при простом удалении сращивания без кавычек (~@
) двойной список не нужен. Вот рабочий автономный пример:
(let [name-metadata 'name-metadata
name 'name]
`(do
~(when name-metadata
`(.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata))))
=> (do (.resetMeta (clojure.lang.Namespace/find (quote name)) name-metadata))
Итак, почему clojure.core выбирает этот, казалось бы, посторонний способ ведения дел?
Это артефакт условности? Есть ли другие подобные случаи, когда это используется более сложным образом?
~
всегда выдает форму; ~@
потенциально может вообще ничего не испускать. Таким образом, иногда используется ~@
для условного объединения в одно выражение:
;; always yields the form (foo x)
;; (with x replaced with its macro-expansion-time value):
`(foo ~x)`
;; results in (foo) is x is nil, (foo x) otherwise:
`(foo ~@(if x [x]))
Вот что здесь происходит: вызов (.resetMeta …)
излучается в форме do
, в которую ns
расширяется только в том случае, если name-metadata
является истинным (не-false
, не-nil
).
В данном случае это не имеет особого значения - можно использовать ~
, опустить лишние скобки и принять, что макрорасширение формы ns
без метаданных имени будет иметь дополнительный nil
в форме do
. Однако ради более красивого расширения имеет смысл использовать ~@
и генерировать форму для обработки метаданных имени только тогда, когда это действительно полезно.
Вы можете задать этот вопрос в списке рассылки Clojure: groups.google.com/forum/#!forum/clojure