我正在学习Haskell,遇到了以下代码:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
就它的工作方式而言,我在解析时遇到了一些麻烦。这很整洁,我知道不需要什么了,但是我想了解Haskell在写时如何设法“填充” fib:
take 50 fibs
有什么帮助吗?
谢谢!
Answers:
我将对其内部如何工作进行一些解释。首先,您必须意识到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的现有知识: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,然后意识到它需要部分评估重击。为了完全求值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添加了唯一的编号,以避免混淆。
不久前,我写了一篇文章。你可以在这里找到它。
正如我在这里提到的,请阅读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仅根据需要计算足够的序列。
在这里可以找到一个非常相关的示例,尽管我还没有完全解决它,但这可能有所帮助。
我不确定具体的实现细节,但我怀疑它们应该与我下面的论点相符。
请带一点盐,这可能在实现上不准确,但只是为了帮助理解。
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
。
如果有人知道一些适当的实施细节,我将非常感兴趣。我可以理解,惰性评估技术可能相当复杂。
希望这有助于理解。
再次强制性免责声明:请带一点盐,这在实现上可能不准确,但只是为了帮助理解。
让我们来看看 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)))
等等。