我正在学习Haskell,并且遇到了以下代码:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
我解析时遇到了一些麻烦,就其工作原理而言。它非常整洁,我知道不需要更多内容了,但我想了解Haskell在写作时如何设法“填写”文件:
take 50 fibs
任何帮助?
谢谢!
答案 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目前对三种表达式的了解:fibs
,tail fibs
和zipWith (+) 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_list
和0 : 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
替换zipWith
和xs = 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)))
等等。