Clojure宏在运行罐子时很奇怪

时间:2015-10-19 14:00:59

标签: clojure macros leiningen uberjar

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

非常感谢任何帮助。

谢谢!

2 个答案:

答案 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)
但是,如果这个解决方案足够稳定并且没有边缘情况,我不确定自己。虽然在这个简单的例子中它确实有效。