这是在 Haskell 中编写斐波那契的一种非常惯用的方式。
基本上,在 Haskell 中,人们试图尽可能地利用惰性(事实数据仅在需要时计算)。
例如,如果你写 a = 1 : a,你会得到一个无限的 1 流 (1:1:1:1:...)。但是,如果您执行take 100 a,您会得到正好 100 个 1,并且程序不会挂起。这是因为 Haskell 不关心列表的其余部分,除非您要求。
那么它与您的问题有什么关系呢?好吧,让我们手动运行这个定义好吗?
lazee = 2:5:(zipWith (+) lazee (tail lazee))
首先,您将 2 和 5 放入 lazee。所以现在我们有
lazee = 2 : 5 : ...
tail lazee = 5 : ...
(我使用... 来显示尚未计算的值)
然后你做了一些非常棘手的事情:你根据它本身来定义 lazee 。我发现这实际上比单纯的递归更漂亮,因为你将递归抽象出来,只留下非常清晰的模式。
ZipWith 是一个将两个列表与一个函数一起压缩的函数
所以
zipWith (+) lazee (tail lazee) = zipWith (+) (2 : 5 : ...) (5 : ...) = 7 ...
那么现在
lazee = 2, 5, 7, ...
tail lazee = 5, 7, ...
zipWith (+) lazee (tail lazee) = zipWith (+) (2, 5, 7, ...) (5, 7, ...) = 7 : zipWith (+) (5, 7, ...) (7, ...) = 7 : 12 : ...
以此类推,Haskell 继续创建 lazee,将它当前在 lazee 上读取的术语添加到下一个。
这是您可能经常在一个班轮中看到的模式:以自身表示的常量值。一个常见的例子是非常低效的素数列表
primes = sieve [2..]
where sieve (p:xs) = p : (filter ((== 1) . (gcd p)) $ sieve xs)
它有什么作用?那么filter (== 1) . (gcd p) 只取与 p 互质的值(即那些 gcd 大于 p 的 1)
所以首先我们有
sieve [2..] = 2 : (filter ((== 1) . (gcd 2) $ sieve [3..])
这意味着我们将 2 作为素数,并且 sieve [3..] 中的每个数字都不是偶数。 sieve [3..] 是 3 后跟每个不与 3 互质的数字。
如果我们在一个小例子上运行它:
sieve [4..] = 4 : ...
sieve [3..] = 3 : 4 : ...
sieve [2..] = 2 : 3 : ...
注意4 是如何被删除的。
我希望这会有所帮助。如果你想看一些有趣的单线,考虑看看https://wiki.haskell.org/Blow_your_mind
我个人建议您避免直接递归(即,您只需使用更改的参数调用自己的递归),以支持您发布的模式和列表操作。
(例如写fact n = product [1..n]来定义阶乘)
在 Haskell 旅途愉快;)