新手问题理解 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)
产生的序列类型不同的实现细节是什么?
- Range 在 clojure.core 中实现。
(range)
定义为 (iterate inc' 0)
.
- iterate 的功能最终由 Iterate.java 中的 Iterate class 提供。
(range end)
被定义为,当 end 是 long 时,如 (clojure.lang.LongRange/create end)
- LongRange class 住在 LongRange.java。
查看两个 java 文件可以看出 LongRange class 实现了 IChunkedSeq
而 Iterator class 没有。 (练习留给 reader。)
推测
- clojure.lang.Iterator 的实现不分块,因为迭代器可以被赋予任意复杂度的函数,分块的效率很容易被计算多于需要的值所淹没。
(range)
的实现依赖于迭代器而不是自定义优化的 Java class 进行分块,因为 (range)
的情况被认为不够普遍保证优化。
我刚开始学习 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)
产生的序列类型不同的实现细节是什么?
- Range 在 clojure.core 中实现。
(range)
定义为(iterate inc' 0)
.- iterate 的功能最终由 Iterate.java 中的 Iterate class 提供。
(range end)
被定义为,当 end 是 long 时,如(clojure.lang.LongRange/create end)
- LongRange class 住在 LongRange.java。
查看两个 java 文件可以看出 LongRange class 实现了 IChunkedSeq
而 Iterator class 没有。 (练习留给 reader。)
推测
- clojure.lang.Iterator 的实现不分块,因为迭代器可以被赋予任意复杂度的函数,分块的效率很容易被计算多于需要的值所淹没。
(range)
的实现依赖于迭代器而不是自定义优化的 Java class 进行分块,因为(range)
的情况被认为不够普遍保证优化。