新手问题理解 Clojure 惰性序列

Newbie Problem Understanding Clojure Lazy Sequences

我刚开始学习 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 1.1 引入了“分块”序列,

This can provide greater efficiency ... Consumption of chunked-seqs as normal seqs should be completely transparent. However, note that some sequence processing will occur up to 32 elements at a time. This could matter to you if you are relying on full laziness to preclude the generation of any non-consumed results. [Section 2.3 of "Changes to Clojure in Version 1.1"]

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

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

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

但是,为什么会有不同? (range)(range 999) 产生的序列类型不同的实现细节是什么?

  1. Range 在 clojure.core 中实现。
  2. (range) 定义为 (iterate inc' 0).
  3. iterate 的功能最终由 Iterate.java 中的 Iterate class 提供。
  4. (range end) 被定义为,当 end 是 long 时,如 (clojure.lang.LongRange/create end)
  5. LongRange class 住在 LongRange.java

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

推测

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