为何在引用双层列表内使用~@(unquote-splicing操作符)而非~(unquote操作符)
问题:Clojure ns宏中为何在引用的双层列表内使用
~@而非~? 我最近在研读Clojure核心库中ns宏的实现,看到了这段精简后的代码片段:
(defmacro ns "...docstring..." {:arglists '([name docstring? attr-map? references*]) :added "1.0"} [name & references] (let [... ; 参数处理逻辑 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-cla...))))
我疑惑的是:为什么在when表达式外层要用~@(unquote-splicing)而不是~(unquote)呢?
回答
咱们先把~和~@的核心作用掰明白:
~(unquote):把单个表达式的值直接插入到宏展开的代码中。如果这个值是一个集合/列表,它会作为一个完整的嵌套结构被插入。~@(unquote-splicing):专门用于处理集合/列表——它会把集合里的元素逐个展开,直接放到当前代码位置,相当于“去掉外层括号”。
回到ns宏的场景,看这段关键代码:
~@(when name-metadata `((.resetMeta (clojure.lang.Namespace/find '~name) ~name-metadata)))
先看如果用~会发生什么
当name-metadata存在时,when表达式会返回一个嵌套列表:((.resetMeta ...))。如果用~插入,最终展开的do块会变成:
(do (clojure.core/in-ns 'your.namespace) ((.resetMeta (clojure.lang.Namespace/find 'your.namespace) {:meta ...})) (with-loading-context ...))
注意这里的((.resetMeta ...))——这是一个嵌套的列表,Clojure会把内层(.resetMeta ...)的执行结果当作一个函数,然后尝试调用它(不带参数),这显然不是我们想要的,大概率会抛出“不是函数”的错误。
再看用~@的正确效果
~@会把when返回的列表((.resetMeta ...))的外层括号去掉,把里面的(.resetMeta ...)直接插入到do的体中,展开后变成:
(do (clojure.core/in-ns 'your.namespace) (.resetMeta (clojure.lang.Namespace/find 'your.namespace) {:meta ...}) (with-loading-context ...))
这就对了:do块会依次执行每个表达式,.resetMeta调用只是作为一个普通的副作用表达式被执行,不会被当作函数调用。
另外,当name-metadata不存在时,when返回nil,~@会直接忽略这个nil,不会在do块里插入任何内容——这也是~@的一个便利特性,比用~处理nil要更简洁(如果用~的话,插入nil会导致do块里多一个无用的nil表达式,虽然不报错,但没必要)。
总结一下:这里用~@是为了把when返回的列表中的表达式平铺到do的体里,避免嵌套列表导致的错误调用,同时优雅处理nil的情况。
内容的提问来源于stack exchange,提问作者Jessie
相关产品推荐
相关产品推荐

