【问题标题】:Clojure: rest vs. nextClojure:休息与下一个
【发布时间】:2011-05-16 08:47:15
【问题描述】:

我很难理解 Clojure 中 restnext 之间的区别。 The official site's page on laziness 表示偏好可能应该是使用rest,但它并没有真正清楚地解释两者之间的区别。谁能提供一些见解?

【问题讨论】:

  • 这一点不值得单独回答:到目前为止,答案尚未完全明确的是() 为真,而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 现在肯定已经弄清楚了;我正在为其他人添加此评论。)

标签: clojure lazy-evaluation


【解决方案1】:

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

另一方面,rest 总是返回一个 seq,因此在您实际使用 rest 的结果之前无需评估任何内容。换句话说,restnext 更懒惰。

【讨论】:

  • 好的,我想我可能明白你的意思了。你是说next 总是返回一个 cons 单元格,而rest 只返回一个 ISeq(或者它实际上是什么类型)?
  • @Daniel: next 返回nil 或非空seqrest 总是返回一个 seq,它可能是空的。
  • 所以我认为,对于某些序列,确定是否有更多元素是一项昂贵的操作。使用普通的 cons 单元列表,我认为确定您是否已到达列表末尾是微不足道的。但是,对于惰性序列(可能来自惰性序列),这种确定可能会很昂贵。我不太明白 nil 与空序列不同。听起来像带有休息的子序列产生的保证较弱,因此可以更有效。
  • 增加混乱:(drop 1 s)rest 更懒,因为它不会强制部分实现 s。 rest 不保证只实现lazy seq 的第一项。这取决于惰性序列。
  • @DavidTonhofer Using Clojure 1.10.1 this code 将打印“make-empty was called”,但 this code 不会。所以在我看来差异仍然存在。
【解决方案2】:

如果你有这个很容易:

(next '(1))
=> nil

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

(rest '(1))
=> ()

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

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

【讨论】:

  • 只是为了把它打死:(rest nil) => ()
【解决方案3】:

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

rest 将返回序列的剩余部分。如果该序列的那部分尚未实现,rest 不会强制执行。它甚至不会告诉你序列中是否还有更多元素。

next 做同样的事情,但随后强制实现序列中的至少一个元素。所以如果next 返回nil,你就知道序列中没有任何元素了。

【讨论】:

    【解决方案4】:

    我现在更喜欢使用带有递归的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/readable-clojure
    • 我通常使用if-let,如:(loop [lst lst] (if-let [[elem & lst] (seq lst)] (recur lst)))
    【解决方案5】:

    在编写使用“普通”递归(使用堆栈)或使用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
    

    为什么nextrest 更渴望?

    如果评估(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,而是返回缺点。
    • 在获取yfirst 时,除了从已经获得的 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 执行适当的测试。
    • 所以我们完成了,只需返回惰性序列作为结果。
    • 在获得yfirst时,需要对lazy-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
    

    【讨论】:

    • 修复了“懒惰”部分,因为 sepp2k 告诉我我完全错了。
    • RE 是 Clojure 的乐趣示例:从 Clojure 1.7 开始,iterate 不再使用 lazy-seq 实现,因此可以对 next 进行特殊处理。由于iterate永远不会终止,所以它的next不需要实现下一步来满足在序列的下一个返回nil的要求。
    猜你喜欢
    • 2011-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-31
    • 2023-01-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多