Clojure: rest vs next

我很难理解在 Clojure restnext之间的区别。官方网站关于懒惰的页面表明最好使用 rest,但它并没有真正清楚地解释两者之间的区别。有人能提供一些见解吗?

11452 次浏览

正如您链接的页面所描述的,next比(rest的新行为)更严格,因为它需要评估惰性缺陷的结构,以知道是返回 nil还是返回一个连续的缺陷。

另一方面,rest总是返回一个 seq,所以在实际使用 rest的结果之前不需要计算任何值。换句话说,restnext更懒惰。

如果你有这个,就很简单了:

(next '(1))
=> nil

因此,next查看下一个内容,如果该行为空,则返回 nil,而不是空的 seq。这意味着它需要向前看(到它将返回的第一个项) ,这使得它不是完全懒惰的(也许您不需要下一个值,但是 next浪费了向前看的计算时间)。

(rest '(1))
=> ()

rest不向前看,只是返回序列的其余部分。

也许你会想,为什么要在这里用两种不同的东西呢?原因是您通常希望知道 seq 中是否没有剩余内容,然后只返回 nil,但是在某些情况下,性能非常重要,多评估一个项目可能意味着您可以使用 rest付出巨大的努力。

next就像 (seq (rest ...))

rest将返回序列的剩余部分。如果这段序列还没有实现,rest不会强迫它。它甚至不会告诉你序列中是否还有更多的元素。

next做同样的事情,但是强迫序列中的至少一个元素被实现。因此,如果 next返回 nil,您就知道序列中已经没有其他元素了。

我现在更喜欢使用带递归的 next,因为转义计算更简单/干净:

(loop [lst a-list]
(when lst
(recur (next lst))

(loop [lst a-list]
(when-not (empty? lst)   ;; or (when (seq? lst)
(recur (rest lst))

但是,如果将集合用作队列或堆栈,就需要使用 rest。在这种情况下,您希望函数在弹出或退出最后一项时返回一个空集合。

当编写使用“ mundane”递归(使用堆栈)或使用 recur(使用尾部递归优化,因此实际上是循环)遍历序列的代码时,下面是一个有用的小表。

rest, next, first, seq, oh my!

请注意 restnext的行为差异。结合 seq,这导致了以下习语,其中列表结束通过 seq测试,列表的其余部分通过 rest获得(改编自“ The Joy of 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

为什么 nextrest更急切?

如果计算 (next coll),结果可以是 nil。必须立即知道这一点(即必须实际返回 nil) ,因为调用方可能基于 nil的真实性进行分支。

如果计算 (rest coll),结果不能是 nil。除非调用方然后使用函数调用测试结果是否为空,否则惰性序列中“ next element”的生成可以延迟到实际需要的时间。

例子

一个完全懒惰的集合,所有的计算都“在需要之前暂停”

(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”的计算在第一个“惰性序列”中暂停。

使用热切的下一步,我们看到两个评价:

(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的 con,右侧为惰性序列。
  • 如果右分支为空,next可能必须返回 nil。因此我们需要检查更深一层。
  • 第二个惰性序列被计算,导致打印出 second lazy-seq evaluated
  • 这导致了一个非空结构: 左边是一个2的 con,右边是一个弱序列。
  • 所以不要返回 nil,而是返回缺点。
  • 当获取 yfirst时,除了从已获取的 con 中检索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的 con,右侧为惰性序列。
  • rest永远不会返回 nil,即使右边的惰性序列的计算结果为空序列。
  • 如果调用者需要了解更多信息(seq 是空的吗?),他可以稍后在惰性序列上执行适当的测试。
  • 这样我们就完成了,结果就是返回惰性序列。
  • 当获得 yfirst时,需要进一步计算惰性 seq 才能获得2

借一步说话

请注意,y的类型是 LazySeq。这似乎是显而易见的,但是 LazySeq不是“语言的东西”,它是“运行时的东西”,表示的不是结果,而是计算的状态。事实上,(type y)clojure.lang.LazySeq只是意味着“我们还不知道类型,你必须做更多的找出”。每当像 nil?这样的 Clojure 函数遇到类型为 clojure.lang.LazySeq的东西时,就会进行计算!

附言。

在 Joy of Clojure,第2版,第126页,有一个使用 iterate的例子来说明 nextrest之间的区别。

(doc iterate)
;=> Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free
;   of side-effects

事实证明,这个例子不起作用。在这种情况下,实际上在 nextrest之间的行为没有什么不同。不知道为什么,也许 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