在你的想法里,请分享对< 《2024年Clojure状况调查!》.

欢迎!请参阅关于页面以获取更多有关此工作方式和作用的信息。

+1
编译器

考虑以下基本项目

(ns whatever.core
  (:gen-class))

(def some-var (doto (System/getenv "FOO") println))

(defn -main [& args] (println some-var))

如果我把这个项目做成uberjar,我会看到在编译过程中,nil被打印(正如预期的那样)。然而,当我在具有环境变量的环境中运行它时,我期望看到nil,但我看到正确的值被打印出来,并且some-var实际上被重新绑定!尽管听起来有点‘方便’(对于这个用例),但我实在无法理解为什么会这样……既然我AOT了命名空间,我本以为我必须推迟直到运行时获取环境变量(例如,通过delay或类似的东西),以便进行这项工作。此外,这种双重加载的影响是什么?如果我在一个def中有一些非常昂贵的计算,并且真的想预先只计算一次(且仅计算一次)怎么办?据我所知,当程序启动时,所有的def表达式(可以从-main到达的)都会重新加载,而不考虑AOT……请帮帮我理解这一点,这让我抓狂!

提前感谢……

1 个回答

+1

没有双重加载或重新加载。Clojure中的代码编译实际上会运行代码(这意味着它会运行所有顶级形式的代码,除非它在顶层被调用,否则它实际上不会运行你的-main函数)。运行uberjar中的代码,嗯,也会运行代码。这是一项完全独立的步骤,独立的加载。

双重加载的影响是什么?

最主要的是,命名空间声明应该没有副作用并且是引用透明的(除非通过所有的def隐式地改变了当前命名空间)。

如果我有一个在 def 里的非常昂贵的计算,并且真的只想预先计算一次(仅此一次),那会怎么样呢?

如果你不想在编译时执行这个昂贵的计算,你可以使用 delay
如果你想在编译期间内联计算的输出,你可以使用宏。

> 那么在一个 uberjar 中运行代码,嗯,也是运行代码。

但是执行应该从 `-main` 方法开始,不是吗?为什么变量会重新绑定?这是因为它根绑定是 null,所以它们被认为是未绑定的,这导致表达式需要重新评估吗?

> 如果你不想在编译时运行这个昂贵的计算,你可以使用 delay。

是的当然,但这不是重点——我应该不能用一个普通的 `def` 来做到这一点吗?比如说,我在计算一些第 n 个斐波那契数,这需要几秒钟。我为什么需要在编译和运行时都支付这个成本?当我有 AOT 编译时,后者就没有意义了,对吗?我遗漏了什么?
> 但是执行应该从 `-main` 方法开始,不是吗?

是的,主功能的执行是这样。运行所有 `ns` 和 `def` 形式也是执行。运行任何其他类型的高级代码也是执行。Clojure 的编译和执行是交织在一起的——这与其他语言的许多处理方式不同。这也是我们拥有令人惊叹的 REPL 体验的原因之一,这里的 "E" 部分与一开始就在某个 uberjar 中运行相同的代码没有差别。反之亦然——在正常情况下运行编译后的工件将不会与通过 REPL、逐行按正确顺序运行的原始代码有区别。

> 为什么变量会重新绑定?

它们并不是重新绑定。它们只是被绑定。uberjar 编译过程和使用该 uberjar 的过程是两个不同的过程,有相同的变量的两个不同的运行时版本。这与变量的值无关。

也许本节中的第二段会有所帮助:https://clojure.org/reference/evaluation

> 假设我在计算某第 n 个斐波那契数,这需要几秒钟。为什么我需要在编译和运行时都付出这个代价?

首先,计算任何 N 的斐波那契数都不应该需要几秒钟。 :) 当然,但这不是重点。
“为什么”这个问题没有 straightforward 的答案,因为这并不是一个像“是的,必须支付两次成本,用户必须承受”这样的自觉决策。这只是 Clojure 的编译/评估方法的后果。

大多数情况下这类事情完全不重要,因为顶层代码都是纯粹的引用透明def,计算并不会花费任何值得注意的时间。
如果某些特定情况下并非如此,我提到了相应的解决方案——`延迟`和宏。
by
那么,您的意思是获取环境变量直到运行时(如下所示)是完全不必要的吗?

>  (def some-var (delay (System/getenv "FOO"))))
by
如果编译项目所在的环境不会影响编译本身,那么就完全没必要了。一个相反的例子——您可以在编译时将 `some-var` 设置为某个环境变量的值,然后在宏中使用 `some-var` 来确定如何展开某些内容。当然,这个方法很不推荐。
by
非常感谢您抽出时间——非常感激 :)
...