斐波那契函数的惰性评估
Lazy evaluation of Fibonacci function
我在 Haskell 中编写了下面的函数,但我不太清楚为什么它不适用于惰性求值。
infFib :: [Integer]
infFib = infFib' []
where
infFib' [] = infFib' [0,1]
infFib' (xs) = infFib' (xs ++ [(foldr (\a b -> a+b) 0 (take 2 (reverse xs)))])
tl;dr 因为 infFib'
从不 return,它只会继续拨打电话。
我预计您在构建 infFib'
时的想法大致是这样的:
- 我想要一个无限列表。
- 我知道列表是如何开始的,以及列表应该如何增长。
- 我将编写一个函数来稍微增加列表,然后迭代它。
- 糟糕,Haskell 不进行迭代——它进行递归。好吧,我会写一个函数来稍微增加列表,然后用它递归。
不幸的是,按照您编写它的方式,您通过使用越来越长的列表调用自己来递归 - 但您永远不会 return (甚至部分)该列表给调用者!您只需不断构建一个越来越长的列表并对其调用 infFib'
。
您应该做的是开始 return 列表中您有信心的部分,并增加您还没有信心的部分。例如:
infFib' [] = [0,1] ++ infFib' [0,1]
infFib' xs = newPart ++ infFib' (xs ++ newPart) where
newPart = [(foldr (\a b -> a+b) 0 (take 2 (reverse xs)))]
此时,您可以在 ghci 中测试 infFib
是否确实有效并生成正确的答案。如果这符合您的需要,您可以到此为止。
但与可能的实现相比,此实现效率极低:在这里,infFib'
的参数列表越来越长,而它只真正关心最后两个元素。然后它必须遍历整个列表才能访问它关心的元素,这会浪费时间和内存。让我们通过只传递最后两个元素来解决这个问题。我们必须修复任何函数调用 infFib'
—— 幸运的是,在这种情况下,因为它在本地作用域为 where
块,我们知道只有两个, infFib'
本身和区块的所有者,infFib
。我们还必须内联 []
的基本情况,因为在那种情况下我们还没有要传递的两个元素!
infFib = [0,1] ++ infFib' 0 1 where
infFib' secondToLast last = [newPart] ++ infFib' last newPart
where newPart = secondToLast + last
我会花一点时间思考如何清理它。我不确定它是否非常明显,但是让代码更清晰的一件事是将每次迭代中产生的元素 infFib'
向左移动两个——而不是产生第三个元素第一次调用时的斐波那契数列,在第一次调用时产生第一个元素。这将消除对额外 [0,1] ++
和特殊 where 块的需要。所以:
infFib = infFib' 0 1 where
infFib' old new = [old] ++ infFib' new (old+new)
最后一个:将 [x] ++ y
的定义展开为 x:y
是非常惯用的。这给了我们最终的定义
infFib = infFib' 0 1 where
infFib' old new = old : infFib' new (old+new)
此时我会感到满意并继续我需要编写的下一个函数。
我在 Haskell 中编写了下面的函数,但我不太清楚为什么它不适用于惰性求值。
infFib :: [Integer]
infFib = infFib' []
where
infFib' [] = infFib' [0,1]
infFib' (xs) = infFib' (xs ++ [(foldr (\a b -> a+b) 0 (take 2 (reverse xs)))])
tl;dr 因为 infFib'
从不 return,它只会继续拨打电话。
我预计您在构建 infFib'
时的想法大致是这样的:
- 我想要一个无限列表。
- 我知道列表是如何开始的,以及列表应该如何增长。
- 我将编写一个函数来稍微增加列表,然后迭代它。
- 糟糕,Haskell 不进行迭代——它进行递归。好吧,我会写一个函数来稍微增加列表,然后用它递归。
不幸的是,按照您编写它的方式,您通过使用越来越长的列表调用自己来递归 - 但您永远不会 return (甚至部分)该列表给调用者!您只需不断构建一个越来越长的列表并对其调用 infFib'
。
您应该做的是开始 return 列表中您有信心的部分,并增加您还没有信心的部分。例如:
infFib' [] = [0,1] ++ infFib' [0,1]
infFib' xs = newPart ++ infFib' (xs ++ newPart) where
newPart = [(foldr (\a b -> a+b) 0 (take 2 (reverse xs)))]
此时,您可以在 ghci 中测试 infFib
是否确实有效并生成正确的答案。如果这符合您的需要,您可以到此为止。
但与可能的实现相比,此实现效率极低:在这里,infFib'
的参数列表越来越长,而它只真正关心最后两个元素。然后它必须遍历整个列表才能访问它关心的元素,这会浪费时间和内存。让我们通过只传递最后两个元素来解决这个问题。我们必须修复任何函数调用 infFib'
—— 幸运的是,在这种情况下,因为它在本地作用域为 where
块,我们知道只有两个, infFib'
本身和区块的所有者,infFib
。我们还必须内联 []
的基本情况,因为在那种情况下我们还没有要传递的两个元素!
infFib = [0,1] ++ infFib' 0 1 where
infFib' secondToLast last = [newPart] ++ infFib' last newPart
where newPart = secondToLast + last
我会花一点时间思考如何清理它。我不确定它是否非常明显,但是让代码更清晰的一件事是将每次迭代中产生的元素 infFib'
向左移动两个——而不是产生第三个元素第一次调用时的斐波那契数列,在第一次调用时产生第一个元素。这将消除对额外 [0,1] ++
和特殊 where 块的需要。所以:
infFib = infFib' 0 1 where
infFib' old new = [old] ++ infFib' new (old+new)
最后一个:将 [x] ++ y
的定义展开为 x:y
是非常惯用的。这给了我们最终的定义
infFib = infFib' 0 1 where
infFib' old new = old : infFib' new (old+new)
此时我会感到满意并继续我需要编写的下一个函数。