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中进行一些非常昂贵的计算,并真正想要预先计算一次(并且只计算一次),会怎样?据我所知,所有可以从-main访问的def表达式在程序启动时都会重新加载,无论是否进行了AOT……请帮我理解这一点,因为它让我痛苦不堪!

提前感谢……

1 答案

+1

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

这种双重加载的影响是什么?

主要影响是,命名空间声明应该没有副作用且引用透明(除非所有def会影响当前命名空间)。

如果我在一个def中有些非常昂贵的计算,并且真的只想预先计算一次(并且只计算一次),怎么办?

如果您不希望复杂的计算在编译时运行,可以使用delay
如果希望在编译期间内联计算的結果,可以使用宏。

包络物
> 在一个uberjar中运行代码,也会运行代码

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

> 如果您不希望在编译时进行昂贵的计算,可以使用delay。

是的,但那已经脱离了主题——我是否可以用普通的`def`来做?比如说,我在计算某个第n个斐波那契数,这可能需要几秒钟。为什么我需要在编译和运行时都支付这个代价?如果我有AOT编译,后者难道没有意义吗?我在哪里忽视了这个问题?
头像
> 但是执行应该从`-main`方法开始,不是吗?

是的,这里是执行`_main_`功能。运行所有`ns`和`def`形式也是执行。运行任何其他类型的顶层代码也是执行。Clojure的编译和执行是相互交织的——这与许多其他语言处理方式不同。这也是我们拥有出色的REPL体验的原因之一,其中“E”部分与代码一开始就在某些uberjar中运行没有区别。反之亦然——在正常情况下,运行编译后的件将与通过REPL逐行运行原始代码的方式相同。

> 为什么变量被重绑定了?

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

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

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

首先,计算任何N的斐波那契数不该需要几秒钟。 :) 当然,但这不是重点。
关于“为什么”的部分没有一个简单的答案,因为这并不是一个像“是的,必须支付两次代价,用户必须受苦”这样的故意决策。这仅仅是因为Clojure的编译/评估方法的后果。

在大多数情况下,这类事情完全可以忽略不计,因为所有的顶层代码都是简单的纯引用透明`def`,它们的计算速度不会引起注意。
如果在某些非常具体的情况下发生了这种情况,有我提到的解决方案——`delay`和宏。


>  (def some-var (delay (System/getenv "FOO"))))

...