【问题标题】:Clojure macros weirdness when running jars运行 jar 时的 Clojure 宏怪异
【发布时间】:2016-01-17 22:05:33
【问题描述】:

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

任何帮助将不胜感激。

谢谢!

【问题讨论】:

  • 宏用于在编译期间修改“clojure-source-code”。因为op 没有要修改的输入,所以我问你——在你看来,op 应该做什么?宏(因为它们在编译时“运行”)在 java 字节码/类文件中不存在。编辑:捕获或产生“来自/进入外部范围的东西”的宏也称为非卫生宏,处理起来特别繁琐。
  • 不纯的宏是个很邪恶的东西。不过,可能你只是忘了用反引号 ` 字符引用它。
  • @leonid-beschastny - 我试图通过 op 宏来反映 mixfix 使用的行为。
  • @birdspider - 感谢您解释什么是非卫生宏。

标签: clojure macros leiningen uberjar


【解决方案1】:

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

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

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

因此,您得到了一个不纯的宏,在您的代码编译时修改fs atom。

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

【讨论】:

  • 谢谢 - 是的,引用宏会产生所需的行为。我已经联系了库维护者,看看是否可以修改(因为我可能在库设计中遗漏了一些东西)。
  • 感谢您强调编译和执行之间的区别。
【解决方案2】:

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

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

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

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

如果仍然坚持宏,使这个想法奏效的方法是跳过发生宏扩展的模块的 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)

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

【讨论】:

  • 谢谢,require 确实适用于 uberjar,只是在我的原始项目的上下文中尝试过。我同意这有点hacky。
  • 我会将此答案标记为已接受,因为这是我现在选择遵循的方法。列昂尼德的回答也很好,所以两者都赞成:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-28
  • 2012-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多