我现在正在阅读“了解你的哈斯克尔”一书,我很好奇这个特定的例子是如何运作的。本书首先演示了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
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中是否有某种神奇的东西使得这种实现效果不如我想的那么低效?
答案 0 :(得分:10)
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 ..])
变为"done"
,因为第一个函数不使用z
,因此永远不需要递归调用。
答案 1 :(得分:3)
如果我正确理解了foldr实现,它将每次迭代整个列表,即使它与它遇到的第一个元素匹配。
这是错误的。 foldr
只会根据需要对列表进行评估。
E.g。
foldr (&&) True [True, False, error "unreached code here"]
返回False
,因为永远不会评估错误,就像在
(True && (False && (error "unreached code here" && True)))
实际上,由于从未达到列表的结尾,我们也可以写
foldr (&&) (error "end") [True, False, error "unreached code here"]
仍然获得False
。
答案 2 :(得分:3)
以下代码说明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>
答案 3 :(得分:2)
使用以下定义(使用标准递归)来考虑foldr:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f e [] = e
foldr f e (x:xs) = f x (foldr f e xs)
第三行显示findKey的第二个实现将在找到第一个匹配时返回。
作为旁注:假设您对findKey有以下定义(没有相同的功能)(作为练习,您可能希望使用foldr重写定义):
findKey :: (Eq k) => k -> [(k,v)] -> [v]
findKey key [] = []
findKey key ((kHead, vHead):rest) = if (key == kHead) then vHead:(findKey key rest) else findKey key rest
现在您可能会认为这会遍历整个输入列表。根据您调用此函数的方式,可能会迭代整个列表,但同时这也可以有效地为您提供第一个匹配。由于Haskell的懒惰评估,以下代码:
head (findKey key li)
会给你第一场比赛(假设有一场比赛)与第一场比赛的效率相同。
答案 4 :(得分:2)
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
在 forcing acc
之前返回,acc
仍未被强制,即没有任何一部分可以访问超出其head元素a
的list参数,例如当你有
f a acc = ...
另一方面,如果您的f
确实强迫其第二个参数,例如如果它被定义为
f a (x:xs) = ...
然后在acc
开始工作之前强制f
,并且在处理开始之前完整访问 列表 - 整体而言,因为acc = f b acc2
和 f
的调用必须强制其第二个参数acc2
,所以它的值为{{ 1}},可以强制(与acc
模式匹配,即);等等。