以下是使用lein new mw
创建的简单Clojure应用示例:
(ns mw.core
(:gen-class))
(def fs (atom {}))
(defmacro op []
(swap! fs assoc :macro-f "somevalue"))
(op)
(defn -main [& args]
(println @fs))
并在project.clj
我有
:profiles {:uberjar {:aot [mw.core]}}
:main mw.core
在REPL中运行时,评估@fs
会返回{:macro-f somevalue}
。但是,运行uberjar会产生{}
。如果我将op
定义更改为defn
而不是defmacro
,那么fs
在从uberjar运行时会再次提供正确的内容。那是为什么?
我隐约意识到这与AOT编译有关,而且在编译阶段就发生了宏扩展这一事实,但显然我对这些事情的理解是缺乏的。
我在尝试部署使用非常好的mixfix库的应用程序时遇到了这个问题,其中mixfix运算符是使用全局原子定义的。我花了很长时间才将问题与上面提到的例子隔离开来。
非常感谢任何帮助。
谢谢!
答案 0 :(得分:6)
这里的真正问题是您的宏不正确。你忘了添加反引号:
(defmacro op []
`(swap! fs assoc :macro-f "somevalue"))
; ^ syntax-quote ("backquote")
这个操作叫做syntax-quote,在这里非常重要,因为clojure中的宏在编译时会修改你的代码。
所以,结果你得到了一个不纯的宏,只要你的代码编译就修改fs
原子。
由于您的宏没有生成任何代码,因此您的示例中的(op)
调用根本不执行任何操作(只有编译执行此操作)。它似乎在REPL中工作,因为编译和执行由同一个clojure实例处理(有关详细信息,请参阅Timur's answer)。
答案 1 :(得分:2)
这确实与AOT有关,并且在执行顶级代码时会出现一些副作用 - 这是在宏扩展时。 lein repl
(或lein run
)与uberjar之间的差异在时恰好发生这种情况。
执行lein repl
时,REPL会启动,然后自动加载mw.core
命名空间(如果在project.clj
中定义,或者手动完成)。加载名称空间时,首先定义原子,然后展开宏,这个扩展会改变原子的值。所有这些都发生在相同的运行时环境中(在REPL过程中),并且在加载模块之后,atom在此REPL中具有更新的值。执行lein run
将完全相同 - 加载命名空间,然后在同一进程中执行-main
函数。
当lein uberjar
被执行时 - 同样的事情发生了,这就是现在的问题。编译器,为了编译clj
文件,将首先加载它并评估顶层(我自己从这个SO answer中学到了它)。因此,模块被加载,顶层被评估,宏被扩展,参考值被改变然后,在编译完成之后,编译器进程(参考值刚刚改变的那个)结束。现在,当使用java -jar
执行uberjar时,这会生成新进程,使用已编译的代码,宏已经用完(因此(op)
已经被替换为#34;代码生成的op
宏,在这种情况下为none)。因此,原子值不变。
在我看来,良好的解决方案是不依赖于宏中的副作用。
如果坚持使用宏,那么让这个想法发挥作用的方法就是跳过宏扩展发生的模块的AOT 和从主模块懒洋洋地加载它(同样的解决方案,如同在我提到的另一个SO answer中)。例如:
project.clj
:
; ...
:profiles {:uberjar {:aot [mw.main]}}) ; note, no `mw.core` here
; ...
main.clj
:
(ns mw.main
(:gen-class))
(defn get-fs []
(require 'mw.core)
@(resolve 'mw.core/fs))
(defn -main [& args]
(println @(get-fs)))
core.clj
:
(ns mw.core
(:gen-class))
(def fs (atom {}))
(defmacro op []
(swap! fs assoc :macro-f "somevalue"))
(op)
但是,如果这个解决方案足够稳定并且没有边缘情况,我不确定自己。虽然在这个简单的例子中它确实有效。