4

下面是一个简单的 Clojure 应用程序示例,使用以下命令创建lein new mw

(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 运算符是使用全局原子定义的。我花了很长时间才将问题与上述示例隔离开来。

任何帮助将不胜感激。

谢谢!

4

2 回答 2

6

这里真正的问题是您的宏不正确。您忘记添加反引号字符:

(defmacro op []
  `(swap! fs assoc :macro-f "somevalue"))
; ^ syntax-quote ("backquote")

此操作称为语法引用,在这里非常重要,因为 clojure 中的宏在编译期间会修改您的代码。

所以,结果你得到了一个不纯的宏,在你的代码被编译fs时修改 atom 。

由于您的宏不会产生任何代码,因此(op)您的示例中的调用根本不执行任何操作(只有编译执行)。它似乎在 REPL 中工作,因为编译和执行由同一个 clojure 实例处理(有关详细信息,请参阅Timur的答案)。

于 2015-10-19T15:10:22.900 回答
3

这确实与 AOT 有关,并且在执行顶级代码时会出现一些副作用 - 这里是在宏扩展时。lein repl(或lein run)和 uberjar之间的区别在于发生这种情况的确切时间。

执行时lein repl,REPL 启动并mw.core自动加载命名空间,如果它是在 中定义的project.clj,或者手动加载。加载命名空间时,首先定义原子,然后扩展宏,此扩展更改原子的值。所有这些都发生在相同的运行时环境中(在 REPL 进程中),并且在加载模块后,原子在这个 REPL 中具有更新的值。执行lein run几乎相同 - 加载命名空间,然后-main在同一进程中执行函数。

何时lein uberjar执行-同样的事情发生了,这就是现在的问题。编译器,为了编译clj文件将首先加载它并评估顶层(我自己从这个SO 答案中学到了它)。所以模块被加载,顶层被评估,宏被扩展,引用值被改变,然后,在编译完成后,编译器过程,刚刚改变引用值的那个,结束。现在,当 uberjar 使用它执行时,会java -jar产生新进程,其中包含已编译的代码,其中宏已经被扩展(因此(op)已经被宏生成的代码“替换”,op在这种情况下没有)。因此,原子值不变。

在我看来,好的解决方法是不要依赖宏中的副作用

如果仍然坚持使用宏,那么使这个想法起作用的方法是跳过发生宏扩展的模块的 AOT,从主模块中延迟加载它(再次,与我提到的其他SO 答案中的解决方案相同)。例如:

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)

但是,我不确定这个解决方案是否足够稳定并且没有边缘情况。在这个简单的例子中它确实有效。

于 2015-10-19T14:47:58.527 回答