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实际上被重新绑定!虽然这听起来有点“方便”(对于这个用例),但我无法理解为什么会发生这种情况……由于我已经编译了命名空间,我本以为需要将获取环境变量的操作推迟到运行时进行(例如,通过delay或其他方式),以便进行这项操作。此外,这种双加载的后果是什么?如果我有一个在def中非常昂贵的计算,我真的很想一次性(且仅一次)预先计算它,该怎么办?据我所知,所有可达自-main的def表达式在程序启动时会重新加载,这与AOT无关……请帮助我理解这一点,因为它让我非常抓狂!

提前表示感谢……

1 答案

+1

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

这种双加载有什么影响?

主要影响是命名空间声明应该是无副作用的和引用透明的(除非通过所有def隐式地改变了当前命名空间)。

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

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

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

但执行应该从`-main`方法开始,对吗?为什么变量会被重新绑定?是不是因为根绑定是空的,因此假设它们没有绑定,这导致了表达式被重新评估?

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

是的,当然,但那并不是问题的关键 —— 难道我不能用普通的`def`来做这件事吗?比如说,我在计算第n个斐波那契数,这需要几秒钟。为什么我需要在编译和运行时都支付这种成本?当我有AOT编译时,这难道不是没有道理的吗?我在错过什么吗?
> 但执行应该从`-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")))
如果编译项目的环境不会影响实际的编译过程,则这完全是不必要的。一个相反的例子 - 你可以在编译时将`some-var`设置为某个环境变量的值,然后在宏中使用它来决定如何展开某些内容。当然,这种做法极不推荐。
非常感谢您的时间 - 非常感谢 :)
...