【问题标题】:Clojure tail recursion with lazy-seqClojure 尾递归与惰性序列
【发布时间】:2018-03-10 10:56:51
【问题描述】:

您能否更广泛、更清楚地解释一下,lazy-seq 如何根据此文档页面使尾递归“安全”?

https://clojuredocs.org/clojure.core/lazy-seq

;; The following defines a lazy-seq of all positive numbers.  Note that 
;; the lazy-seq allows us to make a recursive call in a safe way because
;; the call does not happen immediately but instead creates a closure.

user=> (defn positive-numbers 
    ([] (positive-numbers 1))
    ([n] (lazy-seq (cons n (positive-numbers (inc n))))))
#'user/positive-numbers

user=> (take 5 (positive-numbers))
(1 2 3 4 5)

【问题讨论】:

  • 当您进行函数调用(在大多数语言中)时,您会将一个新帧压入调用堆栈。如果您重复生成无限的数字序列(递归或循环),您最终会溢出。通过使用惰性求值,您的递归次数仅与 take 的参数一样多。
  • @JaredSmith 实际上,无论take 的参数如何,您都只会向堆栈添加少量恒定数量的帧。你自己很容易看出你的解释是行不通的:试着写(last (take 1e6 (range))),并观察即使你的JVM没有空间容纳一百万个堆栈帧,你仍然得到正确的答案。
  • @amalloy 我的立场是正确的。

标签: clojure


【解决方案1】:

如果您查看lazy-seq 的实现,您会发现它确实返回了一个闭包(一个保留一段上下文的函数):

(list 'new 'clojure.lang.LazySeq (list* '^{:once true} fn* [] body))

所以在(cons n (positive-numbers (inc n)) 中对positive-numbers 的调用不会立即评估,而是延迟到闭包被调用。

【讨论】:

    【解决方案2】:

    如果您查看lazy-seq 的源代码,您会注意到它是一个将其参数封装在函数体中的宏:

    user=> (source lazy-seq)
    (defmacro lazy-seq
      "Takes a body of expressions that returns an ISeq or nil, and yields
      a Seqable object that will invoke the body only the first time seq
      is called, and will cache the result and return it on all subsequent
      seq calls. See also - realized?"
      {:added "1.0"}
      [& body]
      (list 'new 'clojure.lang.LazySeq (list* '^{:once true} fn* [] body)))
    

    这会产生这样的结果:

    user=> (macroexpand '(lazy-seq (cons 1 (lazy-seq [2 3 4]))))
    (new clojure.lang.LazySeq (fn* [] (cons 1 (lazy-seq [2 3 4]))))
    

    这会提示您正在发生的事情:尾部位置的执行被推迟到需要时。这是如何实现的?看clojure/lang/LazySeq.java (Copyright (c) Rich Hickey. All rights reserved)

    final synchronized Object sval(){
      if(fn != null)
        {
                    sv = fn.invoke();
                    fn = null;
        }
      if(sv != null)
        return sv;
      return s;
    }
    
    final synchronized public ISeq seq(){
      sval();
      if(sv != null)
        {
        Object ls = sv;
        sv = null;
        while(ls instanceof LazySeq)
          {
          ls = ((LazySeq)ls).sval();
          }
        s = RT.seq(ls);
        }
      return s;
    }
    

    这两个方法执行可调用以获取尾部值——如果它们看到一个,它们也会展开包含的 LazySeq。然后缓存结果。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-18
      • 2012-03-22
      • 1970-01-01
      相关资源
      最近更新 更多