Clojure:休息与下一个


70

我很难理解Clojurerest和之间的区别next官方网站上关于懒惰的页面表明,偏好可能应该是使用rest,但是并不能真正清楚地解释两者之间的区别。谁能提供一些见识?


4
这一点不值得单独回答:到目前为止,尚未完全明确给出答案的()是正确的,而nil错误的是。因此,(defn keep-going [my-coll] (if (rest my-coll) (keep-going (rest my-coll)) "Finished.")它将永远继续下去-或更确切地说,将使堆栈溢出-同时(defn keep-going [my-coll] (if (next my-coll) (keep-going (next my-coll)) "Finished.")完成。(OP现在肯定已经解决了这个问题;我正在为其他人添加此评论。)
Mars

Answers:


62

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

rest另一方面,总是返回一个seq,因此在您实际使用的结果之前,不需要进行任何评估rest。换句话说,rest比懒next


好吧,我想我可能理解你的意思。您是说next总是返回一个cons单元格,而rest仅返回一个ISeq(或它实际上是什么类型)吗?
Daniel Yankowsky 2010年

1
@Daniel:next返回nil或非空seqrest始终返回seq,可能为空。
sepp2k 2010年

因此,我认为,对于某些序列,确定是否还有更多元素是一项昂贵的操作。在使用普通的con单元格列表的情况下,我认为确定您是否到达列表的末尾很简单。但是,对于延迟序列(可能来自lazy-seq),此确定可能会很昂贵。我不太明白nil与空序列不同。听起来,子序列产生的静息产生了较弱的保证,因此可以提高效率。
Daniel Yankowsky 2010年

5
(drop 1 s)令人困惑的是:比起rest它不强制实现s的部分实现,它更为懒惰。rest不能保证只实现延迟序列的第一项。这取决于延迟序列。
2011年

@DavidTonhofer使用Clojure 1.10.1,此代码将打印“调用了make-empty”,但此代码不会。因此在我看来,差异仍然存在。
sepp2k

33

这很简单:

(next '(1))
=> nil

所以 next请看下一件事情,如果该行为空,它将返回nil而不是一个空序列。这意味着它需要向前看(返回到它返回的第一项),这使其不能完全懒惰(也许您不需要下一个值,但next浪费了计算时间来向前看)。

(rest '(1))
=> ()

rest 不会向前看,只会返回其余的序列。

也许您认为,为什么还要在这里使用两种不同的东西呢?原因是您通常想知道seq中是否还剩下什么而只是返回nil,但是在某些情况下,性能非常重要,评估另外一项可能意味着您可以付出巨大的努力rest


7
只是打败它:(rest nil)=>()
gtrak

22

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

rest将返回序列的剩余部分。如果该序列的一部分尚未实现,请rest不要强行执行。它甚至不会告诉您序列中是否还有其他元素。

next做同样的事情,但是迫使序列中的至少一个要素得以实现。因此,如果nextreturn nil,您将知道序列中没有其他元素了。


3

我现在更喜欢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,不过,会如果你使用一个集合作为一个队列或堆栈中。在这种情况下,您希望函数在弹出或出列最后一个项目时返回空集合。


第一个示例如果lst为空,则执行无用的递归调用,因为空列表是真实的。测试“非零”的一种好方法:((when (some? lst) ...通过tonsky.me/blog/可读性clojure
David Tonhofer

我通常使用if-let,例如: (loop [lst lst] (if-let [[elem & lst] (seq lst)] (recur lst)))
Daniel Yankowsky,

1

当编写使用“平凡”递归(使用堆栈)或使用recur(使用尾递归优化,从而实际上循环)遍历序列的代码时,此表非常有用。

休息,接下来,首先,接着,我的天哪!

注意的行为差异restnext。与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。除非调用者随后使用函数调用测试结果是否为空,否则可以将lazy-seq中“下一个元素”的生成延迟到实际需要的时间。

一个完全懒惰的集合,所有计算都“保持暂停直到需要”

(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

现在,在第一个“ lazy-seq”处暂停“ x”的计算。

在此之后,我们将使用以下急切方法,进行两次评估:

(def y (next x))

;=> first lazy-seq evaluated
;=> second lazy-seq evaluated
;=> #'user/y

(type y)

;=> clojure.lang.Cons

(first y)

;=> 2
  • 评估第一个lazy-seq,从而导致打印输出 first lazy-seq evaluated
  • 这将导致一个非空结构:左侧为1的cons,右侧为lazy-seq的cons。
  • nextnil如果右分支为空,则可能必须返回。因此,我们需要更深入地检查一个级别。
  • 评估第二个lazy-seq,导致打印输出 second lazy-seq evaluated
  • 这导致了一个非空结构:一个cons左边有2个,而lazy-seq在右边。
  • 因此,不要返回nil,而是返回缺点。
  • 当获得的firsty,除了从已获得的缺点中检索2之外,没有任何其他操作。

使用lazier rest,我们看到一个评估(请注意,必须首先重新定义x才能使这项工作生效)

(def y (rest x))

;=> first lazy-seq evaluated
;=> #'user/y

(type y)

;=> clojure.lang.LazySeq

(first y)

;=> second lazy-seq evaluated
;=> 2
  • 评估第一个lazy-seq,从而导致打印输出 first lazy-seq evaluated
  • 这将导致一个非空结构:左侧为1的cons,右侧为lazy-seq的cons。
  • rest永远不会返回nil,即使右侧的lazy-seq求值为空seq。
  • 如果呼叫者需要了解更多信息(seq是否为空?),则可以稍后在lazy-seq上执行适当的测试。
  • 这样就完成了,只返回lazy-seq作为结果。
  • 当获得的firsty,需要进一步评估lazy-seq,以获取2

侧边栏

请注意,y类型为LazySeq。这看起来似乎很明显,但LazySeq不是“语言的事物”,它是“运行时的事物”,不是表示结果而是一种计算状态。实际上(type y)clojure.lang.LazySeq只是意味着“我们还不知道类型,您必须做更多的工作才能找出答案”。每当Clojure函数之类的nil?命中类型clojure.lang.LazySeq就会发生计算!

聚苯乙烯

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

事实证明,该示例不起作用。在这种情况下,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

修复了“懒惰”部分,因为sepp2k告诉我我完全错了。
David Tonhofer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.