首页 > 解决方案 > 了解 Clojure 惰性序列的新手问题

问题描述

我刚刚开始学习 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 个整数,则会出现错误。

为什么结果不一样?

标签: clojurelazy-sequences

解决方案


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

这可以提供更高的效率......作为普通 seqs 的 chunked-seqs 的消耗应该是完全透明的。但是,请注意,某些序列处理一次最多会发生 32 个元素。如果您依靠完全的懒惰来排除任何非消耗结果的生成,这可能对您很重要。[ “1.1 版中对 Clojure 的更改”第 2.3 节]

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

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

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

但是,为什么会有差异?产生的 seq 有哪些实现细节(range)和不同之处?(range 999)

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

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

猜测

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

推荐阅读