我刚刚开始学习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引入了"分块"序列,
这可以提供更高的效率...分块序列的消耗为 正常的序列应该是完全透明的。但是,请注意,一些 序列处理一次最多可以进行 32 个元素。这可以 如果你依靠完全懒惰来排除 生成任何未消耗的结果。["1.1版对Clojure的更改"第2.3节]
在您的示例中(range)
似乎正在生成一个一次实现一个元素的 seq,(range 999)
正在生成一个分块的 seq。map
将一次消耗一个分块的序列,从而生成一个分块的序列。因此,当 take 询问分块 seq 的第一个元素时,传递给 map 的函数在值 0 到 31 上被调用 32 次。
我相信以这样的方式编码是最明智的,如果该函数产生具有任意大块的分块 seq,那么代码仍然适用于任何产生 seq 的函数/arity。
我不知道一个人是否编写了一个不分块的 seq 生成函数,如果一个人可以依靠当前和未来版本的库函数(如 map 和过滤器)不将 seq 转换为分块的 seq。
但是,为什么会有差异?实现细节是什么,以至于(range)
和(range 999)
在生成的序列类型上有所不同?
- 范围在 clojure.core 中实现。
(range)
定义为(iterate inc' 0)
。- 最终,Iterate的功能由Iterate.java中的Iterate类提供。
- 当结束为长时,
(range end)
定义为(clojure.lang.LongRange/create end)
远程 - 类住在远程.java。
查看这两个 java 文件可以看出,LongRange 类实现了IChunkedSeq
,而 Iterator 类没有。 (练习留给读者。
投机
- clojure.lang.Iterator 的实现不会分块,因为迭代器可以被赋予任意复杂度的函数,并且分块的效率很容易被计算比需要的更多的值所淹没。
(range)
的实现依赖于迭代器,而不是执行分块的自定义优化 Java 类,因为(range)
情况被认为不够常见,无法保证优化。