Haskell 使用fold是否比标准递归效率低
我现在正在阅读《向你学习哈斯克尔》一书,我很好奇这个特殊的例子是如何工作的。本书首先演示了使用传统递归实现的Haskell 使用fold是否比标准递归效率低,haskell,recursion,Haskell,Recursion,我现在正在阅读《向你学习哈斯克尔》一书,我很好奇这个特殊的例子是如何工作的。本书首先演示了使用传统递归实现的findKey: findKey :: (Eq k) => k -> [(k,v)] -> Maybe v findKey key [] = Nothing findKey key ((k,v):xs) = if key == k then Just v
findKey
:
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) = if key == k
then Just v
else findKey key xs
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f e [] = e
foldr f e (x:xs) = f x (foldr f e xs)
然后,本书使用foldr
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
使用标准递归,函数应该在使用提供的键命中第一个元素后立即返回。如果我正确理解了foldr
实现,它每次都会遍历整个列表,即使它与遇到的第一个元素匹配。这似乎不是一个非常有效的解决问题的方法
关于foldr
实现是如何工作的,我有什么不了解的吗?或者Haskell中是否有某种魔力使得这个实现不像我认为的那么低效
如果我正确理解foldr实现,它每次都会遍历整个列表,即使它与遇到的第一个元素匹配
这是错误的foldr
将根据需要对列表进行评估
例如
返回False
,因为从未计算错误,与中的错误完全相同
(True && (False && (error "unreached code here" && True)))
事实上,由于列表的末尾永远不会到达,我们也可以写
foldr (&&) (error "end") [True, False, error "unreached code here"]
并且仍然获得False
foldr
使用标准递归编写foldr
的递归调用隐藏在acc
中。如果您的代码不使用acc,它将永远不会被计算(因为Haskell是懒惰的)。因此,foldr
版本是高效的,并且会提前返回Prelude> foldr (\x z -> "done") "acc" [0 ..]
"done"
此表达式立即返回“done”
,即使输入列表无限长
如果
foldr
定义为:
foldr f z (x : xs) = f x (foldr f z xs)
foldr _ z [] = z
,然后进行评估
f x (foldr f z xs)
where
f = \x z -> "done"
x = 0
z = "acc"
xs = ... -- unevaluated, but is [1 ..]
那是
(\x z -> "done") 0 (foldr (\x z -> "done") "acc" [1 ..])
因为第一个函数不使用
z
,所以它就变成了“done”
,所以不需要递归调用。下面的代码证明了foldr
确实“短路”了findKey
的计算:
import Debug.Trace
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
tr x = trace msg x
where msg = "=== at: " ++ show x
thelist = [ tr (1,'a'), tr (2,'b'), tr (3, 'c'), tr (4, 'd') ]
在ghci中运行findKey
的示例:
*Main> findKey 2 thelist
=== at: (1,'a')
=== at: (2,'b')
Just 'b'
*Main>
考虑使用以下定义(使用标准递归)的foldr: 第三行显示findKey的第二个实现将在找到第一个匹配项时返回
旁注:假设您对findKey有以下定义(没有相同的功能)(作为练习,您可能希望使用foldr重写定义): 现在您可能认为这将遍历整个输入列表。根据调用此函数的方式,它可能会遍历整个列表,但同时也可以高效地为您提供第一个匹配。由于Haskell的惰性评估,以下代码:
head (findKey key li)
将以与第一个示例相同的效率为您提供第一个匹配(假设有一个匹配)
foldr f z [a,b,c,...,n] ==
a `f` (b `f` (c `f` (... (n `f` z) ...))) ==
f a (foldr f z [b,c,...,n]) ==
f a acc where acc = foldr f z [b,c,...,n]
因此,如果您的f
在acc
之前返回,则acc
将保持不强制状态,即不会访问列表参数头元素a
之外的任何部分,例如
f a acc = ...
另一方面,如果您的f
确实强制其第二个参数,例如,如果它被定义为
f a (x:xs) = ...
然后在
f
开始工作之前强制执行acc
,并且在处理开始之前将访问整个列表——总体而言,因为acc=f b acc2
,调用f
必须强制执行其第二个参数acc2
,因此可以强制执行其值acc
(与(x:xs)
匹配的模式,即);等等。我需要一直盯着它看一段时间,直到它被理解,但我想我明白了你所说的递归被内置到acc
中的意思。谢谢!注意:编译器不能使用泛型递归定义的任何属性。但是编译器知道如何通过重写规则来处理foldr
ch意味着通过foldr
定义函数可以让编译器更好地优化代码。
f a acc = ...
f a (x:xs) = ...