我没有找到足够清楚的当前答案,所以这里......
sequence
确实返回了一个 LazySeq,但它是一个分块的,所以当你在 REPL 中使用它时,你通常会觉得它是渴望的,因为你的集合可能太小了,而分块会让它看起来很渴望. 我认为块大小有点动态,它并不总是完全相同大小的块,但通常它似乎大小为 32。所以你的转换器将一次应用于输入集合 32 个元素,懒洋洋。
这是一个简单的转换器,它只打印它减少的元素并原封不动地返回它们:
(defn printer
[xf]
(fn
([] (xf))
([result] (xf result))
([result input]
(println input)
(xf result input))))
如果我们用它创建一个s
包含 100 个元素的序列:
(def s
(sequence
printer
(range 100)))
;;> 0
我们看到它打印0
,但没有别的。在调用 时sequence
,第一个元素将从 中消耗(range 100)
,并将传递给xf
要转换的链,在我们的例子中只是打印它。因此,除了第一个元素外,没有其他元素被消耗。
现在,如果我们从 中获取一个元素s
:
(take 1 s)
;;> 0
;;> 1
;;> 2
;;> 3
;;> 4
;;> 5
;;> 6
;;> 7
;;> 8
;;> 9
;;> 10
;;> 11
;;> 12
;;> 13
;;> 14
;;> 15
;;> 16
;;> 17
;;> 18
;;> 19
;;> 20
;;> 21
;;> 22
;;> 23
;;> 24
;;> 25
;;> 26
;;> 27
;;> 28
;;> 29
;;> 30
;;> 31
;;> 32
我们看到它打印了前 32 个元素。这是 Clojure 中分块惰性序列的正常行为。您可以将其视为半惰性,因为它一次消耗块大小的元素,而不是一次消耗 1 个。
现在,如果我们尝试从 1 到 32 中获取任何元素,则不会打印任何其他内容,因为前 32 个元素已被处理:
(take 1 s)
;; => (0)
(take 10 s)
;; => (0 1 2 3 4 5 6 7 8 9)
(take 24 s)
;; => (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
(take 32 s)
;; => (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
没有任何东西被打印出来,每次拍摄都返回预期的结果集。我;; =>
用于返回值和;;>
打印输出。
好的,现在如果我们采用第 33 个元素,我们希望看到下一个 32 个元素的块被打印:
(take 33 s)
;;> 33
;;> 34
;;> 35
;;> 36
;;> 37
;;> 38
;;> 39
;;> 40
;;> 41
;;> 42
;;> 43
;;> 44
;;> 45
;;> 46
;;> 47
;;> 48
;;> 49
;;> 50
;;> 51
;;> 52
;;> 53
;;> 54
;;> 55
;;> 56
;;> 57
;;> 58
;;> 59
;;> 60
;;> 61
;;> 62
;;> 63
;;> 64
惊人的!所以再一次,我们看到只取了接下来的 32 个,这使我们现在总共处理了 64 个元素。
好吧,这表明sequence
使用转换器调用实际上会创建一个惰性分块序列,其中元素只会在需要时被处理(一次块大小)。
那么这是关于什么的?:
生成的序列元素是增量计算的。这些序列会根据需要增量消耗输入,完全实现中间操作。此行为不同于惰性序列上的等效操作。
这是关于操作发生的顺序。带sequence
和换能器:
(sequence (comp A B C) coll)
块中的每个元素都会让它们通过: A -> B -> C
,所以你得到:
A(e1) -> B(e1) -> C(e1)
A(e2) -> B(e2) -> C(e2)
...
A(e32) -> B(e32) -> C(e32)
而对于普通的惰性序列,例如:
(->> coll A B C)
将首先让所有分块元素通过 A,然后让它们都通过 B,然后通过 C:
A(e1)
A(e2)
...
A(e32)
|
B(e1)
B(e2)
...
B(e32)
|
C(e1)
C(e2)
...
C(e32)
这需要在每个步骤之间进行中间收集,因为必须将 A 的结果收集到一个集合中,然后循环并应用 B 等。
我们可以在前面的示例中看到这一点:
(def s
(sequence
(comp (filter odd?)
printer
(map vector)
printer)
(range 10)))
(take 1 s)
;;> 1
;;> [1]
;;> 3
;;> [3]
;;> 5
;;> [5]
;;> 7
;;> [7]
;;> 9
;;> [9]
(def l
(->> (range 10)
(filter odd?)
(map #(do (println %) %))
(map vector)
(map #(do (println %) %))))
(take 1 l)
;;> 1
;;> 3
;;> 5
;;> 7
;;> 9
;;> [1]
;;> [3]
;;> [5]
;;> [7]
;;> [9]
看看第一个会怎样,filter -> vector -> filter -> vector, etc.
而第二个会怎样filter all -> vector all
。好吧,这就是文档中引用的意思。
现在还有一件事,两者之间的分块应用方式也有所不同。使用sequence
和转换器,它将处理元素,直到转换器结果具有块大小的元素计数。而在惰性序列的情况下,它将在每个级别上分块处理,直到所有步骤都有足够的时间来完成他们需要做的事情。
这就是我的意思:
(def s
(sequence
(comp printer
(filter odd?))
(range 100)))
(take 1 s)
;;> 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
(def l
(->> (range 100)
(map #(do (print % "") %))
(filter odd?)))
(take 1 l)
;;> 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
在这里,我将打印逻辑修改为在同一行,因此不会占用太多空间。如果仔细观察,s
处理了输入范围的 66 个元素,而l
只消耗了 32 个元素。
原因就是我上面说的。使用sequence
,我们将继续获取块,直到我们获得块大小的结果。在这种情况下,块大小为 32,并且由于我们过滤odd?
,我们需要两个块才能达到 32 个结果。
使用lazy-seq,它不会尝试获取第一块结果,而是从输入中获取足够多的块来满足逻辑,在这种情况下,我们只需要从输入中获取 32 个元素中的一块即可找到单奇数取。