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会隐式地改变当前的命名空间)。

如果在定义中有一些非常昂贵的计算,并且真的很想只计算一次(而且只有一次),该怎么办呢?

如果您不希望在编译时执行昂贵的计算,可以使用延迟
如果您希望在编译期间内联计算结果,可以使用宏。

by
> 在uberjar中运行代码,嗯,也运行代码

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

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

是的,当然,但这不是重点 - 我不可以用普通的`def`做吗?比如说我在计算第n个斐波那契数,这需要几秒钟。为什么我既要付出编译时的代价,也要付出运行时的代价?当我有AOT编译时,后者没有意义,对吧?我在错过什么?
by
> 但执行应该从`-main`方法开始,不是吗?

主功能的执行是这样。运行所有`ns`和`def`形式也是执行。运行任何其他顶级代码也是执行。Clojure的编译和执行是交织在一起的 - 它不同于许多其他语言的处理方式。这也是我们在REPL中有很好的体验的原因之一,那里的"E"部分和一开始就在某个uberjar中有相同代码的体验是一样的。反之亦然 - 通常情况下运行编译后的工件将与通过REPL逐个形式运行原始代码没有区别。

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

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

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

> 假设我在计算some nth斐波那契数,这需要几秒钟。为什么我既要付出编译时的代价,也要付出运行时的代价?

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

绝大多数情况下,这类事情完全不重要,因为所有顶级代码都是普通的引用透明(referentially transparent)的 `def` 形式,不需要计算就完成了。
如果由于某种原因,在特定情况下不是这样,我已提到有解决方案——`delay` 和宏(macros)。
来自
所以,您是说在运行时获取环境变量(如下),完全是不必要的吗?

>  (def some-var (delay (System/getenv "FOO")))
来自
如果您编译项目所在的环境不会影响编译本身,则是不必要的。反例的例子——您可以在编译时将 `some-var` 设置为某些环境变量的值,然后在宏中使用 `some-var` 来确定如何展开某些内容。当然,这强烈不推荐。
来自
非常感谢您的宝贵时间——非常感谢 :)
...