懒惰和stackoverflow

6

我写了以下内容:

(fn r [f xs]
  (lazy-seq
    (if (empty? xs)
    '()
    (cons (f (first xs)) (r f (rest xs))))))

为了解决4clojure.com的问题#118:http://www.4clojure.com/problem/118,需要重新实现不使用map等的map函数,并且该解决方案通过了测试(我不知道它是否正确:它非常接近其他解决方案)。因为问题要求必须是lazy,所以我通过将我的解决方案包装在lazy-seq中来编写上面的代码......但是我不明白这里的“lazy”是什么,也不知道如何测试它。当我询问(type ...)时,我得到的是一个clojure.lang.LazySeq,但我不知道它和如果我简单地删除lazy-seq“包装”得到的结果有什么区别。当然,如果我删除lazy-seq,尝试执行以下操作会导致stackoverflow:
(= [(int 1e6) (int (inc 1e6))]
   (->> (... inc (range))
        (drop (dec 1e6))
        (take 2)))

否则(也就是说:如果我让惰性序列包装保持不变),它似乎可以正常工作。
因此,我决定尝试以某种方式“调试”/跟踪正在发生的事情,以尝试理解它是如何工作的。我使用了以下宏(我记得是在stackoverflow上找到的):
(defmacro dbg [x] `(let [x# ~x] (println "dbg: " '~x "=" x#) x#))

我将工作版本包裹在dbg宏中并尝试再次执行。现在糟糕了:之前正常工作的版本现在也会抛出堆栈溢出。
我不确定:也许这是宏的一个不良影响,它会以某种方式强制评估本来不会被评估的东西?
如果有人能够使用这个简单的函数和简单的测试来解释懒惰是如何工作的,什么时候确切地调用了什么等等,那就太好了。

Clojure有'realized?'测试函数,用于测试惰性序列的值是否已被实现。例如,(realized? (range))将返回false。 - NielsK
1个回答

4
整个魔法在于clojure.lang.LazySeq java类。它本身实现了ISeq接口,而s-expressions参数被转换为一个没有任何参数的函数,并传递给clojure.lang.LazySeq的构造函数(采用IFn对象作为参数的构造函数),因为最终你又调用了返回一个ISeq而不是完整列表的r函数,这允许LazySeq延迟评估项。因此,基本流程大致如下:
  • LazySeq调用传递给它的Fn(即代码的其余部分)
  • 此Fn调用返回ISeq,因为List实现了ISeq。这个返回的ISeq(列表)具有第一个值作为具体值,第二个是LazySeq对象,因为对r的递归调用。这个返回的ISeq存储在类中的局部变量中。
  • LazySeq的ISeq实现在调用下一个项目时会调用上面一步中存储在本地类变量中的ISeq(列表)的下一个,并检查它是否是LazySeq类型(由于r调用,它将是第2个项目),如果是LazySeq,则评估该项并返回该项,否则直接返回该项(您传递给cons的第一个具体值)

我知道这有点令人费解 :). 我刚才也浏览了Java代码,并且能够在意识到递归调用r本身返回懒惰序列后弄清楚。所以你拥有它,一种自定义分界符连续性 :)


可能有必要澄清这个习语中没有涉及到递归,因为r的返回值是一个LazySeq,只有在头部被实现时才会再次调用r。实现总是发生在r已经返回之后。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接