使用折叠比标准递归效率低

时间:2016-07-16 16:31:56

标签: 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  
                            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中是否有某种神奇的东西使得这种实现效果不如我想的那么低效?

5 个答案:

答案 0 :(得分:10)

  1. foldr使用标准递归编写。

  2. foldr的递归调用隐藏在acc内。如果你的代码没有使用acc,它将永远不会被计算(因为Haskell是懒惰的)。因此foldr版本效率很高,也会提前返回。

  3. 以下是一个证明这一点的例子:

    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模式匹配,即);等等。