了解递归定义的列表(以zipWith表示的文件)


70

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

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

就它的工作方式而言,我在解析时遇到了一些麻烦。这很整洁,我知道不需要什么了,但是我想了解Haskell在写时如何设法“填充” fib:

take 50 fibs

有什么帮助吗?

谢谢!

Answers:


120

我将对其内部如何工作进行一些解释。首先,您必须意识到Haskell使用一种叫做thunk的东西作为其值。一个thunk基本上是一个尚未计算的值-将其视为0参数的函数。无论何时Haskell想要,它都可以评估(或部分评估)该重击,将其转化为实际价值。如果仅部分评估一个重击,则结果值中将包含更多重击。

例如,考虑以下表达式:

(2 + 3, 4)

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

使用fib,它有点复杂,因为它是递归的,但是我们可以使用相同的想法。由于不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,然后意识到它需要部分评估重击。为了完全求值zipWith (+) fibs (tail fibs),它需要对前两行进行求和-不能完全做到这一点,但是可以开始对前两行求和:

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

请注意,我在第三行中填写了“ 1”,它也自动出现在第一行和第二行中,因为所有三行都共享相同的重击(将其视为已写入的指针)。由于未完成评估,因此创建了一个包含其余内容的新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>

依此类推,直到它填满第三行的48列,从而算出前50个数字。Haskell会根据需要进行尽可能多的评估,并在需要时将序列的无限“剩余”保留为重对象。

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

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


嘿,为什么这样做?fibs = 0:1:1:2:<thunk>尾部fibs = 1:1:1:2:<thunk> zipWith(+)fibs(tail fibs)= 1:2:<thunk>不应该最后一行(“结果行”)是这样的:zipWith(+)fibs(tail fibs)= 1:2:3:<thunk>因为我可以加1 +2。为什么它会创建一个新的<thunk>?并且不应该将此“结果行”附加到原始列表(文件)吗?像这样:0:1:1:2:1:2:3:<thunk>(最后4个值,包括<thunk>是zipwith(+)的结果...)很抱歉所有这些问题:x
jdstaerk

而新线似乎并不明显,以工作的意见..很抱歉,太:/
jdstaerk

1
是的,注释语法很烦人。“不是最后一行……是……因为我可以加1 +2。” -啊但是仅仅因为运行时可以在Haskell中做某事并不意味着它可以。这就是“惰性评估”的重点。我的意思是,最终它会,但是在那个阶段,我仅展示“获取4条小纤维”的计算,该计算只需要评估“ zipWith(+)条小纤维(尾部小纤维)”的2个元素。我不明白你的其他问题;您无需将zipWith附加到小纤维上,而是将其附加到1:2即可制成最终的小纤维。
mgiuca

1
图片的问题是语句“ fibs = 0:1:1:2:x”(其中x是“ zipWith ...”)。那不是fib的定义;它的定义为“小纤维= 0:1:x”。我不确定多余的“:1:2”的来源。可能是因为我写了“ zipWith ... = <thunk>”,然后又写了“ fibs = 0:1:1:2:<thunk>”。是吗 请注意,<thunk>在每个代码块中都是不同的值。每次对thunk求值时,它都会被内部带有新thunk的新表达式替换。我将更新代码,为每个thunk提供一个唯一的编号。
mgiuca

1
好的,谢谢。确实,我被那个笨蛋弄糊涂了。那就是您的见解和帮助。祝你有美好的一天!:)
jdstaerk

22

不久前,我写了一篇文章。你可以在这里找到它。

正如我在这里提到的,请阅读Paul Hudak的书“ The Haskell Expression School”中的14.2章,他在其中以斐波那契例子讨论递归流。

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

| --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------- ----------------------- |
| 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 斐波那契数列(fibs)|
| --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------- ----------------------- |
| 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | Fib序列的尾巴(尾巴纤维)|
| --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------- ----------------------- |

添加两列:添加fib(尾部fib 以获得fib序列的尾巴

| --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------- ----------------------- |
| 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 斐波那契数列的尾部|
| --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------- ----------------------- |

add fibs(tail fibs)可以写为zipWith(+)fibs(tail fibs)

现在,我们需要做的所有工作都是从前两个斐波那契数开始,以获得完整的斐波那契序列。

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

注意:此递归定义不适用于渴望评估的典型语言。它在进行惰性评估时可在haskell中工作。因此,如果您要求输入前4个斐波那契数,则取4个fib,haskell仅根据需要计算足够的序列。


3

这里可以找到一个非常相关的示例,尽管我还没有完全解决它,但这可能有所帮助。

我不确定具体的实现细节,但我怀疑它们应该与我下面的论点相符。

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

Haskell不会对任何事物进行评估,除非被迫进行评估,这就是所谓的“惰性评估”,它本身就是一个漂亮的概念。

因此,假设我们只被要求做一个take 3 fibsHaskell将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

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

希望这有助于理解。

再次强制性免责声明:请带一点盐,这在实现上可能不准确,但只是为了帮助理解。


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)))

等等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.