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) = ...