I was looking throught the source code in clojure.core
for no particular reason.
I started reading defmacro ns
, here is the abridged source:
(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)))))
And then trying to read it I saw some strange macro patterns, in particular we can look at:
~@(when name-metadata
`((.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata)))
clojure.core
versionHere is a standalone working extraction from the macro:
(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))
When I ran this could I couldn't help but wonder why there is a double list at the point `((.resetMeta
.
I found that by just removing the unquote-splicing (~@
) the double list was unnecessary. Here is a working standalone example:
(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))
Thus, why does clojure.core choose this seemingly extraneous way of doing things?
Is this an artifact of convention? Are there other similar instances where this is used in more complex ways?
~
always emits a form; ~@
can potentially emit nothing at all. Thus sometimes one uses ~@
to splice in a single expression conditionally:
;; 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]))
That's what's going on here: the (.resetMeta …)
call is emitted within the do
form that ns
expands to only if name-metadata
is truthy (non-false
, non-nil
).
In this instance, it doesn't really matter – one could use ~
, drop the extra brackets and accept that the macroexpansion of an ns
form with no name metadata would have an extra nil
in the do
form. For the sake of a prettier expansion, though, it makes sense to use ~@
and only emit a form to handle name metadata when it is actually useful.