对 lazy-seq 的调用应该有什么范围?

What scope should calls to lazy-seq have?

我正在编写 Recamán's Sequence 和 运行 的惰性实现,但对于应该在哪里调用 lazy-seq 感到困惑。

我今天早上想到的第一个版本是:

(defn lazy-recamans-sequence []
  (let [f (fn rec [n seen last-s]
            (let [back (- last-s n)
                  new-s (if (and (pos? back) (not (seen back)))
                          back
                          (+ last-s n))]
              (lazy-seq ; Here
                (cons new-s (rec (inc n) (conj seen new-s) new-s)))))]
    (f 0 #{} 0)))

然后我意识到我对 lazy-seq 的放置有点随意,它可以放置得更高以包含更多的计算:

(defn lazy-recamans-sequence2 []
  (let [f (fn rec [n seen last-s]
            (lazy-seq ; Here
              (let [back (- last-s n)
                    new-s (if (and (pos? back) (not (seen back)))
                            back
                            (+ last-s n))]
                  (cons new-s (rec (inc n) (conj seen new-s) new-s)))))]
    (f 0 #{} 0)))

然后我回头看了一个review that someone gave me last night:

(defn recaman []
  (letfn [(tail [previous n seen]
            (let [nx (if (and (> previous n) (not (seen (- previous n))))
                       (- previous n)
                       (+ previous n))]
              ; Here, inside "cons"
              (cons nx (lazy-seq (tail nx (inc n) (conj seen nx))))))]
    (tail 0 0 #{})))

他们在 cons!

的调用中有他们自己的

仔细想想,好像也没什么区别。对于更广泛的范围(如第二个版本),更多代码位于传递给 LazySeq 的显式函数中。然而,范围越窄,函数本身可能越小,但由于传递的函数涉及递归调用,它无论如何都会执行相同的代码。

他们的表现似乎几乎完全相同,并给出了相同的答案。有什么理由更喜欢将 lazy-seq 放在一个地方而不是另一个地方吗?这仅仅是一种风格选择,还是会产生实际影响?

在前两个示例中,lazy-seq 包装了 cons 调用。这意味着当您生成调用该函数时,您会立即 return 一个惰性序列而不计算序列的第一项。

在第一个示例中,let 表达式仍在 lazy-seq 之外,因此第一项的值会立即计算,但 returned 序列仍然是惰性的并且 not realized.

第二个例子与第一个相似。 lazy-seq 包装 cons 单元格和 let 块。这意味着该函数将立即 return 并且仅当调用者开始使用惰性序列时才计算第一项的值。

在第三个例子中,列表中第一项的值是立即计算的,只有 returned 序列的尾部是惰性的。

Is there any reason to prefer placing lazy-seq in one place over another?

这取决于你想达到什么目的。您想立即 return 一个序列而不计算任何值吗?在这种情况下,使 lazy-seq 的范围尽可能广泛。否则尝试将lazy-seq的范围限制为只计算序列的尾部。

当我第一次学习 Clojure 时,我对 lazy-seq 构造的许多可能选择感到有点困惑,在选择哪个构造方面缺乏明确性,并且对如何 [= =11=] 首先造成懒惰(它被实现为 ~240 行的 Java class)。

为了减少重复并使事情尽可能简单,我创建了 the lazy-cons macro。它是这样使用的:

(defn lazy-countdown [n]
  (when (<= 0 n)
    (lazy-cons n (lazy-countdown (dec n)))))

(deftest t-all
  (is= (lazy-countdown  5) [5 4 3 2 1 0] )
  (is= (lazy-countdown  1) [1 0] )
  (is= (lazy-countdown  0) [0] )
  (is= (lazy-countdown -1) nil ))

此版本立即实现了初始值n

我从不担心分块(通常是 32 个批次)或试图精确控制惰性序列中实现的元素数量。恕我直言,如果您需要像这样的细粒度控制,最好使用显式循环而不是对惰性序列中的实现时间进行假设。