【问题标题】:What scope should calls to lazy-seq have?对惰性序列的调用应该有什么范围?
【发布时间】:2018-08-14 15:55:01
【问题描述】:

我正在编写 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 放在一个地方而不是另一个地方?这仅仅是一种风格选择,还是会产生实际影响?

【问题讨论】:

  • 您可以看看我的回答:stackoverflow.com/a/44102122/597473 - 在标题为“这有什么实际区别”的部分中。
  • @PiotrekBzdyl 嗯,最后一点是有道理的。所以,如果每个元素计算起来都很便宜和/或你不在乎一个元素是否不必要地计算,那么真的没有区别吗?但是对于更昂贵的计算,是否应该尽可能多地包裹以延迟计算,直到绝对必要?
  • 我会说默认情况下让它完全惰性,除非你需要(我怀疑)这个语义:产生的惰性序列的第一个元素必须在创建惰性序列的同时实现。否则差异将是一个额外的调用来计算应该便宜的第一个元素。
  • @PiotrekBzdyl 好电话,谢谢。如果您想具体回答这个问题,我很乐意接受。如果没有,当我从远足回来时,我可以写点东西,或者等着看是否有人能写出对差异的良好描述。
  • 我认为我提到的答案已经涵盖了差异,但请随时提供您自己的答案。

标签: clojure lazy-sequences


【解决方案1】:

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

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

第二个例子与第一个相似。 lazy-seq 包装了 cons 单元和 let 块。这意味着该函数将立即返回,并且只有在调用者开始消费惰性序列时才计算第一项的值。

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

有什么理由更喜欢将lazy-seq放在一个地方而不是另一个地方?

这取决于您想要达到的目标。您想立即返回一个序列而不计算任何值吗?在这种情况下,尽可能扩大lazy-seq 的范围。否则尽量限制lazy-seq的范围,只计算序列的尾部。

【讨论】:

  • 我认为第二个例子是不同的,而不是第三个。在示例 #1 中,您声称我们“立即返回惰性序列而不计算序列的第一项”,但实际上第一项是在返回惰性序列之前计算的。它只是存储在惰性序列的闭包中,而不是存储在 cons 单元中。就何时完成多少工作而言,第一个示例相当于第三个示例。
【解决方案2】:

当我第一次学习 Clojure 时,我对 lazy-seq 构造的许多可能选择感到有些困惑,在选择哪个构造方面缺乏明确性,以及对 lazy-seq 如何造成懒惰的解释有些模糊首先(它被实现为大约 240 行的 Java 类)。

为了减少重复并让事情尽可能简单,我创建了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 个批次)或试图精确控制在惰性序列中实现的元素数量。恕我直言,如果您需要像这样的细粒度控制,最好使用显式循环而不是对延迟序列中的实现时间做出假设。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-26
    • 2023-03-17
    • 2012-11-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多