运行 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 实例处理(有关详细信息,请参阅 )。
下面是使用 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
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 实例处理(有关详细信息,请参阅