运行 jar 时 Clojure 宏异常

Clojure macros weirdness when running jars

下面是使用 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 returns {:macro-f somevalue}。但是,运行 一个 uberjar 会产生 {}。如果我将 op 定义更改为 defn 而不是 defmacro,那么当来自 uberjar 的 运行 时,fs 再次具有正确的内容。这是为什么?

我隐约意识到这与AOT编译和宏展开发生在编译阶段之前有关,但显然我对这些东西缺乏理解。

我 运行 在尝试部署使用非常好的 mixfix 库的应用程序时遇到了这个问题,其中 mixfix 运算符是使用全局原子定义的。我花了很长时间才将问题与上面给出的示例隔离开来。

任何帮助将不胜感激。

谢谢!

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

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

当执行 lein uberjar 时 - 同样的事情发生了,这就是现在的问题。编译器,为了编译 clj 文件,首先会加载它并评估顶层(我自己从这个 中学到的)。因此加载模块,评估顶层,扩展,更改参考值,然后在编译完成后,刚刚更改参考值的编译器过程结束。现在,当使用 java -jar 执行 uberjar 时,这会生成新进程,其中包含已编译的代码,其中宏已被扩展(因此 (op) 已经是 "replaced",代码为 op 宏生成,在本例中为 none)。因此,原子值不变。

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

如果无论如何坚持使用宏,实现这个想法的方法是跳过发生宏扩展的模块的 AOT 从主模块延迟加载它 (同样,与我提到的另一个 中的解决方案相同)。例如:

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)

但是,我自己不确定这个解决方案是否足够稳定并且没有边缘情况。它确实适用于这个简单的示例。

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

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

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

因此,您得到了一个不纯的宏,只要您的代码 编译 .

就会修改 fs 原子

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