懒惰序列的思考

时间:2010-02-06 16:35:24

标签: clojure

以Clojure Wiki中的Fibonacci系列为例,Clojure代码为:

(def fib-seq
 (lazy-cat [0 1] (map + (rest fib-seq) fib-seq)))

如果您从[0 1]开始考虑这个问题,它是如何工作的?如果有关于思考过程的建议会用这些术语进行思考,那就太棒了。

3 个答案:

答案 0 :(得分:37)

答案 1 :(得分:12)

我的Clojure有点生疏,但这似乎是着名的Haskell单行的字面翻译:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

[我将使用伪Haskell,因为它更简洁一点。]

你需要做的第一件事就是让懒惰陷入其中。当你看到这样的定义时:

zeroes = 0 : zeroes

作为一名严格的程序员,你的直接反应将是“ZOMG无限循环!必须修复bug!”但它不是一个无限循环。这是 lazy 无限循环。如果你做了像

这样的蠢事
print zeroes

然后,是的, 将是一个无限循环。但只要你只使用有限数量的元素,你就永远不会注意到递归实际上并没有终止。这真的很难得到。我还是没有。

懒惰就像货币体系一样:它基于这样的假设:绝大多数人从不使用他们的绝大多数钱。因此,当将1000美元存入银行时,他们不会将其保存在保险箱中。他们借给别人。实际上,他们利用钱,这意味着他们向其他人借了5000美元。他们只需要足够的钱,以便他们可以快速重新调整它,以便当你看到它时那里 / p>

只要他们能够设法在你走到自动取款机时总是给钱,实际上你的绝大部分钱都不存在并不重要:他们只需要少量你在你退学的时间点。

懒惰的作用是一样的:每当你看到它,价值就在那里。如果你看zeroes的第一个,第十个,第五个,第四个元素,它存在。但它在那里, if 时你看,不是

这就是为什么zeroes的这种无关紧要的递归定义有效的原因:只要你不试图查看 last 元素(或每个元素)无限列表,你是安全的。

下一步是zipWith。 (Clojure的map只是对其他编程语言通常有三种不同功能的概括:mapzipzipWith。在本例中,它用作{ {1}}。)

zipWith系列函数以这种方式命名的原因是因为它实际上像拉链一样工作,这也是如何最好地可视化它。假设我们有一些体育赛事,每个参赛者都有两次尝试,两次尝试的分数相加,以得出最终结果。如果我们有两个序列ziprun1以及每次运行的得分,我们可以像这样计算最终结果:

run2

假设我们的两个列表是res = zipWith (+) run1 run2 (3 1 6 8 6),我们将这两个列表并排排列,比如拉链的两半,然后我们使用给定的函数将它们压缩在一起(在这种情况下(4 6 7 1 3))产生一个新序列:

+

参赛者#3获胜。

那么,3 1 6 8 6 + + + + + 4 6 7 1 3 = = = = = 7 7 13 9 9 看起来像什么?

好吧,它以fib开始,然后我们追加0,然后我们追加无限列表的总和,其中无限列表移动了一个元素。最简单的方法就是:

  • 第一个元素为零:

    1
  • 第二个元素是一个:

    0
    
  • 第三个元素是第一个元素加上其余元素的第一个元素(即第二个元素)。我们将这两个列表放在彼此的顶部,就像拉链一样再次想象这个。

    0   1
    
  • 现在,我们刚刚计算的元素不仅仅是0 1 + 1 = 1 函数的输出,它同时也是输入,因为它被附加到两个列表(实际上是相同的列表,只是移动了一个):

    zipWith
  • 等等:

    0   1   1
    +   +
    1   1
    =   =
    1   2
    

或者如果你以不同的方式绘制它并将结果列表和第二个输入列表(实际上它们是相同的)合并为一个:

0   1   1   2
+   +   +
1   1   2
=   =   =
1   2   3


0   1   1   2   3   ^
+   +   +   +       |
1   1   2   3   ^   |
=   =   =   =   |   |
1   2   3   5---+---+

无论如何,这就是将其可视化的方式。

答案 2 :(得分:3)

至于如何运作:

斐波纳契系列的每个术语显然是添加前两个术语的结果。 这就是map在这里所做的,map将每个元素中的每个元素应用+,直到其中一个序列用完为止(当然,在这种情况下,它们不会)。因此,结果是一系列数字,这些数字是通过将序列中的一个术语添加到序列中的下一个术语而产生的。然后你需要lazy-cat给它一个起点,并确保该函数只返回它所要求的内容。

这个实现的问题是,只要定义了fib-seq,fib-seq就会保留整个序列,所以它最终会让你失去记忆。

Stuart Halloway的书花了一些时间来剖析这个函数的不同实现,我认为最有趣的是下面的(它是Christophe Grande的):

(defn fibo []
    (map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))

与之前发布的实现不同,序列中的元素没有任何保留,因此这个元素可以继续运行而不会产生OutOfMemoryError。

如何用这些术语思考是一个更难的问题。到目前为止,对我来说,这是一个熟悉许多不同的做事方式并尝试它们的问题,同时一般寻找应用现有函数库的方法,而不是使用递归和lazy-cat。但在某些情况下,递归解决方案非常好,所以它取决于问题。我期待着获得 Clojure的喜悦一书,因为我认为这对我这个问题有很大的帮助。

相关问题