请在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的斐波那契数不应该是几秒钟的事情。 :) 但当然,这不是重点。
“为什么”这部分没有直接答案,因为那不是像“是的,必须支付两次代价,用户必须忍受”这样的有意决定。这仅仅是Clojure的编译/评估方法的自然结果。

在这种情况下,这大部分时间都不是很重要,因为所有顶级代码都是纯引用透明的`def`,它们的计算时间不显著。
如果某个特定情况下不是这样,有解决方案,我已经提到了——`delay`和宏。
所以,你是说在运行时获取环境变量(如下所示)是完全没有必要的吗?

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