在编写使用“普通”递归(使用堆栈)或使用recur(使用尾递归优化,因此实际上是循环)遍历序列的代码时,这里有一个小表很有用。
注意rest 和next 的行为差异。结合seq,这会导致以下成语,其中列表末尾通过seq 测试,其余列表通过rest 获得(改编自“Clojure 的喜悦”):
; "when (seq s)":
; case s nonempty -> truthy -> go
; case s empty -> nil -> falsy -> skip
; case s nil -> nil -> falsy -> skip
(defn print-seq [s]
(when (seq s)
(assert (and (not (nil? s)) (empty? s)))
(prn (first s)) ; would give nil on empty seq
(recur (rest s)))) ; would give an empty sequence on empty seq
为什么next 比rest 更渴望?
如果评估(next coll),则结果可能是nil。这必须立即知道(即必须实际返回nil),因为调用者可能会根据nil 的真实性进行分支。
如果计算(rest coll),则结果不能是nil。除非调用者随后使用函数调用测试结果是否为空,否则延迟序列中“下一个元素”的生成可能会延迟到实际需要的时间。
示例
一个完全惰性的集合,所有的计算都“等待需要”
(def x
(lazy-seq
(println "first lazy-seq evaluated")
(cons 1
(lazy-seq
(println "second lazy-seq evaluated")
(cons 2
(lazy-seq
(println "third lazy-seq evaluated")))))))
;=> #'user/x
“x”的计算现在在第一个“lazy-seq”处暂停。
之后使用 eager next,我们看到两个评估:
(def y (next x))
;=> first lazy-seq evaluated
;=> second lazy-seq evaluated
;=> #'user/y
(type y)
;=> clojure.lang.Cons
(first y)
;=> 2
- 第一个惰性序列被评估,导致打印输出
first lazy-seq evaluated
- 这会产生一个非空结构:左侧为 1 的 cons,右侧为惰性序列。
-
如果右分支为空,
next 可能必须返回nil。所以我们需要更深入地检查一个层次。
- 第二个惰性序列被评估,导致打印输出
second lazy-seq evaluated
- 这会产生一个非空结构:左侧为 2 的 cons,右侧为惰性序列。
- 所以不要返回
nil,而是返回缺点。
- 在获取
y 的first 时,除了从已经获得的 cons 中检索 2 之外,什么也做不了。
使用懒惰的rest,我们看到一个评估(请注意,您必须先重新定义x 才能使其工作)
(def y (rest x))
;=> first lazy-seq evaluated
;=> #'user/y
(type y)
;=> clojure.lang.LazySeq
(first y)
;=> second lazy-seq evaluated
;=> 2
- 第一个惰性序列被评估,导致打印输出
first lazy-seq evaluated
- 这会产生一个非空结构:左侧为 1 的 cons,右侧为惰性序列。
-
rest 永远不会返回 nil,即使右侧的惰性序列将评估为空序列。
- 如果调用者需要了解更多信息(seq 是否为空?),他可以稍后对lazy-seq 执行适当的测试。
- 所以我们完成了,只需返回惰性序列作为结果。
- 在获得
y的first时,需要对lazy-seq进一步求值,得到2
侧边栏
注意y 的类型是LazySeq。这似乎很明显,但LazySeq 不是“语言的东西”,它是“运行时的东西”,代表的不是结果而是计算状态。事实上(type y) 是clojure.lang.LazySeq 只是意味着“我们还不知道类型,你必须做更多才能找到”。每当像 nil? 这样的 Clojure 函数遇到 clojure.lang.LazySeq 类型的东西时,就会发生计算!
附言
在 Joy of Clojure,第 2 版,第 126 页,有一个使用 iterate 的示例来说明 next 和 rest 之间的区别。
(doc iterate)
;=> Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free
; of side-effects
事实证明,该示例不起作用。在这种情况下,next 和rest 之间的行为实际上没有区别。不知道为什么,也许next 知道它永远不会在这里返回nil,只是默认为rest 的行为。
(defn print-then-inc [x] (do (print "[" x "]") (inc x)))
(def very-lazy (iterate print-then-inc 1))
(def very-lazy (rest(rest(rest(iterate print-then-inc 1)))))
;=> [ 1 ][ 2 ]#'user/very-lazy
(first very-lazy)
;=> [ 3 ]4
(def less-lazy (next(next(next(iterate print-then-inc 1)))))
;=> [ 1 ][ 2 ]#'user/less-lazy
(first less-lazy)
;=> [ 3 ]4