理解递归定义的列表(就zipWith而言)

时间:2011-06-08 02:35:36

标签: haskell lazy-evaluation

我正在学习Haskell,并且遇到了以下代码:

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

我解析时遇到了一些麻烦,就其工作原理而言。它非常整洁,我知道不需要更多内容了,但我想了解Haskell在写作时如何设法“填写”文件:

take 50 fibs

任何帮助?

谢谢!

4 个答案:

答案 0 :(得分:112)

我会在内部解释它是如何工作的。首先,您必须意识到Haskell使用名为thunk的东西作为其值。 thunk基本上是一个尚未计算的值 - 将其视为0参数的函数。每当Haskell想要时,它都可以评估(或部分评估)thunk,将其转换为实际值。如果只有部分评估thunk,那么结果值将包含更多的thunk。

例如,考虑表达式:

(2 + 3, 4)

在普通语言中,此值将作为(5, 4)存储在内存中,但在Haskell中,它将存储为(<thunk 2 + 3>, 4)。如果你要求该元组的第二个元素,它会告诉你“4”,而不是一起添加2和3。只有当你要求该元组的第一个元素时,它才会评估thunk,并意识到它是5。

对于fibs,它有点复杂,因为它是递归的,但我们可以使用相同的想法。因为fibs不带参数,所以Haskell将永久存储已发现的任何列表元素 - 这很重要。

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

有助于可视化Haskell目前对三种表达式的了解:fibstail fibszipWith (+) fibs (tail fibs)。我们假设Haskell开始了解以下内容:

fibs                         = 0 : 1 : <thunk1>
tail fibs                    = 1 : <thunk1>
zipWith (+) fibs (tail fibs) = <thunk1>

请注意,第二行只是向左移动的第一行,第三行是前两行的总和。

要求take 2 fibs,您将获得[0, 1]。 Haskell不需要进一步评估上述内容就可以找到它。

要求take 3 fibs,Haskell将获得0和1,然后意识到它需要部分评估 thunk。为了完全评估zipWith (+) fibs (tail fibs),它需要对前两行求和 - 它不能完全做到这一点,但它可以开始来对前两行求和:

fibs                         = 0 : 1 : 1: <thunk2>
tail fibs                    = 1 : 1 : <thunk2>
zipWith (+) fibs (tail fibs) = 1 : <thunk2>

请注意,我填充了第3行中的“1”,它也自动出现在第一行和第二行中,因为所有三行共享相同的thunk(想想它就像一个写入的指针)。并且由于它没有完成评估,它创建了一个包含列表的 rest 的新thunk,如果需要的话。

不需要,因为take 3 fibs已完成:[0, 1, 1]。但现在,请你说take 50 fibs; Haskell已经记得0,1和1.但它需要继续下去。所以它继续总结前两行:

fibs                         = 0 : 1 : 1 : 2 : <thunk3>
tail fibs                    = 1 : 1 : 2 : <thunk3>
zipWith (+) fibs (tail fibs) = 1 : 2 : <thunk3>

...

fibs                         = 0 : 1 : 1 : 2 : 3 : <thunk4>
tail fibs                    = 1 : 1 : 2 : 3 : <thunk4>
zipWith (+) fibs (tail fibs) = 1 : 2 : 3 : <thunk4>

依此类推,直到它填满第3行的48列,从而计算出前50个数字。 Haskell根据需要进行评估,并将序列的无限“休息”留作thunk对象,以防它再次需要。

请注意,如果您随后要求take 25 fibs,Haskell将不再对其进行评估 - 它只会从已经计算的列表中获取前25个数字。

编辑:为每个thunk添加了一个唯一的编号,以避免混淆。

答案 1 :(得分:21)

我前一段时间写了一篇文章。你可以找到它here

正如我在那里提到的,请阅读Paul Hudak的书“The Haskell School of Expression”中的第14.2章,其中他使用Fibonacci示例谈论递归流。

注意:序列的尾部是没有第一个项目的序列。

|---+---+---+---+----+----+----+----+------------------------------------|
| 1 | 1 | 2 | 3 |  5 |  8 | 13 | 21 | Fibonacci sequence (fibs)          |
|---+---+---+---+----+----+----+----+------------------------------------|
| 1 | 2 | 3 | 5 |  8 | 13 | 21 | 34 | tail of Fib sequence (tail fibs)   |
|---+---+---+---+----+----+----+----+------------------------------------|

添加两列:添加纤维(尾纤)以获得纤维序列尾部的尾部

|---+---+---+---+----+----+----+----+------------------------------------|
| 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | tail of tail of Fibonacci sequence |
|---+---+---+---+----+----+----+----+------------------------------------|

添加fibs(tail fibs)可以写成zipWith(+)fibs(tail fibs)

现在,我们需要通过从前2个斐波纳契数开始来获得完整的斐波那契数列来完成这一代。

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

注意:此递归定义不适用于执行急切评估的典型语言。它适用于haskell,因为它进行了懒惰的评估。所以,如果你要求前4个斐波纳契数,需要4个纤维,haskell只会根据需要计算足够的序列。

答案 2 :(得分:3)

可以找到一个非常相关的示例here,虽然我没有完全解决它可能会有所帮助。

我不完全确定实施细节,但我怀疑它们应该符合我的论点。

请带上一点盐,这可能在实施上不准确,但只是作为一种理解的帮助。

Haskell不会评估任何东西,除非它被迫,这被称为懒惰评估,这本身就是一个美丽的概念。

所以我们假设我们只被要求做take 3 fibs Haskell将fibs列表存储为0:1:another_list,因为我们已经被要求take 3我们可以并假设它存储为fibs = 0:1:x:another_list(tail fibs) = 1:x:another_list0 : 1 : zipWith (+) fibs (tail fibs)0 : 1 : (0+1) : (1+x) : (x+head another_list) ...

通过模式匹配Haskell知道x = 0 + 1因此引导我们0:1:1

如果有人知道一些正确的实施细节,我会非常感兴趣。我可以理解,懒惰评估技术可能相当复杂。

希望这有助于理解。

强制性免责声明:请带上一点盐,这可能在实施上不准确,但仅作为理解辅助。

答案 3 :(得分:1)

让我们来看看 zipWith 的定义 zipWith f (x:xs) (y:ys) = f x y : zipWith xs ys

我们的纤维是: fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

对于take 3 fibs替换zipWithxs = tail (x:xs)的定义,我们得到了 0 : 1 : (0+1) : zipWith (+) (tail fibs) (tail (tail fibs))

对于take 4 fibs再次替换,我们得到了 0 : 1 : 1 : (1+1) : zipWith (+) (tail (tail fibs)) (tail (tail (tail fibs)))

等等。