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` 方法开始,是吗?为什么变量被重新绑定?是根绑定为空,所以它们被认为是未绑定的,这会导致表达式被重新求值吗?

> 如果您不想在编译时运行昂贵的计算,可以使用延迟 (delay)。

当然,但那已经偏离了重点 —— 我应该不能直接用 `def` 来做这些吗?比如说,我在计算某些第 n 个斐波那契数,这个过程可能需要几秒钟。为什么我需要在编译和运行时都付出这个代价?当我有 AOT 编译时,后者就没有意义了,对吗?我漏掉了什么?
> 但是执行应该从 `-main` 方法开始,对吧?

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

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

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

也许本节第二段会有所帮助:[链接](https://clojure.org/reference/evaluation)

> 举个例子,我正在计算某个第 n 个斐波那契数,这个过程可能需要几秒钟。为什么我既需要在编译时也需要在运行时付出这个代价?

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

绝大多数情况下,这类事情根本无关紧要,因为所有顶层代码都是简单的引用透明 `def`,其计算时间不明显。
如果某些情况下不是这样,我已经提到了解决方案——`delay` 和宏。
作者:
所以,你是在说推迟获取环境变量直到运行时(如下所示),是完全不必要的吗?

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