【问题标题】:Newbie Problem Understanding Clojure Lazy Sequences了解 Clojure 惰性序列的新手问题
【发布时间】:2026-01-12 23:50:02
【问题描述】:

我刚开始学习 Clojure,我对惰性序列的工作原理感到困惑。特别是,我不明白为什么这两个表达式会在 repl 中产生不同的结果:

;; infinite range works OK
(user=> (take 3 (map #(/(- % 5)) (range)))
(-1/5 -1/4 -1/3)

;; finite range causes error
user=> (take 3 (map #(/(- % 5)) (range 1000)))
Error printing return value (ArithmeticException) at clojure.lang.Numbers/divide (Numbers.java:188).
Divide by zero

我采用整数序列(0 1 2 3 ...) 并应用一个减去 5 然后取倒数的函数。显然,如果将其应用于 5,则会导致除零错误。但由于我只从惰性序列中获取前 3 个值,所以我没想到会看到异常。

结果是我使用所有整数时的预期结果,但如果我使用前 1000 个整数,则会出现错误。

为什么结果不同?

【问题讨论】:

  • Clojure 有时会将惰性序列上的操作分块,作为一种优化。为什么 Clojure 会分块一个表达式而不是另一个?我不知道。重要的是,在使用惰性序列时,代码需要使得实现比要求更多的元素仍能产生正确的行为。
  • 感谢 Shannon,这确实解释了我所看到的行为,但我很失望地发现 Clojure 做到了这一点。这似乎违反了参照透明度——我帖子中的两个表达式应该产生相同的结果 IMO。这也意味着文档不是完全诚实的——“范围”文档说它返回一个惰性序列,但实际上这个序列是惰性的而不是惰性的。
  • 我同意这令人困惑,但我认为您不能说它违反了参照透明度。你可以用什么表达式交换它的值来产生不同的结果? “问题”是(range)(range 999) 产生的值的差异比预期的要多,但是您仍然可以将它们中的任何一个替换为它们的值并获得相同的结果。
  • @amalloy 我的问题是:我应该如何看待有异常的序列?如果我认为序列是完全惰性的,那么我的两个表达式都应该起作用。另一方面,如果我认为序列是完全评估的(不是字面意思),那么两个表达式都应该抛出异常。两种解释都不起作用。我想我的问题比参考透明度更基本。我的表达式甚至没有确定性值(因为它们可能会也可能不会根据分块引发异常)。我想我的收获是听从 ShannonSeverance 的建议并小心。

标签: clojure lazy-sequences


【解决方案1】:

Clojure 1.1 引入了“分块”序列,

这可以提供更高的效率...... 正常的序列应该是完全透明的。但是,请注意,有些 序列处理一次最多可处理 32 个元素。这可以 如果您依靠完全的懒惰来排除 生成任何未消耗的结果。 ["Changes to Clojure in Version 1.1"第2.3节]

在您的示例中,(range) 似乎正在生成一个一次实现一个元素的序列,而(range 999) 正在生成一个分块序列。 map 将一次消耗一个分块序列,产生一个分块序列。因此,当 take 请求分块 seq 的第一个元素时,传递给 map 的函数在值 0 到 31 上被调用 32 次。

我相信以这样的方式编码是最明智的,如果该函数产生具有任意大块的分块序列,则该代码仍然适用于任何产生序列的函数/arity。

我不知道是否可以编写一个不分块的 seq 生成函数,是否可以依赖当前和未来版本的库函数(如 map 和 filter)不将 seq 转换为分块 seq。

但是,为什么会有差异? 有哪些实现细节使得(range)(range 999) 在生成的序列类型上有所不同?

  1. 范围在clojure.core中实现。
  2. (range) 定义为 (iterate inc' 0)
  3. 最终 iterate 的功能由 Iterate.java 中的 Iterate 类提供。
  4. (range end) 已定义,当 end 为 long 时,如 (clojure.lang.LongRange/create end)
  5. Lo​​ngRange 类位于LongRange.java

查看两个java文件可以看出,LongRange类实现了IChunkedSeq,而Iterator类没有。 (练习留给读者。)

推测

  1. clojure.lang.Iterator 的实现不会分块,因为迭代器可以被赋予任意复杂度的函数,并且分块的效率很容易被计算出比需要更多的值所淹没。
  2. (range) 的实现依赖于迭代器,而不是执行分块的自定义优化 Java 类,因为人们认为 (range) 的情况不足以保证优化。

【讨论】:

  • 我想我现在有了一个心智模型。涉及 clojure seqs 的表达式将产生与使用完全惰性 seqs 的类似表达式相同的结果,但条件是实现未请求的元素没有可观察到的副作用(如抛出异常)。因此,只要我避免副作用(和异常),我就可以对 seq 进行推理,就好像它们是完全懒惰的一样。我的示例表达式不满足这个条件,所以不同的行为不应该让我感到惊讶。感谢您和@amalloy 的帮助。