【问题标题】:Recursion or fold in haskell在haskell中递归或折叠
【发布时间】:2023-04-09 19:55:01
【问题描述】:

我正在学习一些使用 haskell 的函数式编程,并且我正在尝试通过重新实现一些库函数来了解一些概念。

我有一个问题,主要是关于何时选择递归而不是迭代更好。例如,在重新实现“全部”功能时,我可以选择:

all1 :: (a -> Bool) -> [a] -> Bool
all1 p xs = foldr (\x acc -> acc && p x) True xs 

all2 :: (a -> Bool) -> [a] -> Bool
all2 p (x:xs) 
  | p x == False = False
  | otherwise    = all2 p xs

在我看来,递归的应该更节省时间,因为它会在第一个不匹配的条目处停止,而折叠的更节省空间(我认为,仍然不清楚尾递归优化haskell) 但它总是会扫描完整列表,除非通过查看false 将始终给出false 的事实进行了一些巧妙的优化。

那么,这种妥协是一直存在的吗?我是否误解了递归和折叠工作的方式?

【问题讨论】:

  • Haskell 的这个领域(空间/时间效率)不是我的强项,但我认为如果你将折叠函数重写为(\x acc -> p x && acc),那么如果它找到了一个False 值,因为这个变体将[e1, e2, e3, e4, ...] 变成p e1 && (p e2 && (p e3 && (p e4 && ...))),如果有任何返回False,它就会短路。
  • (1) 旁注:foldr 也是递归的。与使用 foldr 等函数的解决方案相比,谈论第二个解决方案的常用术语是“显式递归”。 (2) 正如@bradrn 所指出的,如果您将参数的顺序切换为(&&),您的第一个解决方案也会短路。在这里的答案中非常值得探索。
  • 另一方面,Haskell 中的尾递归必须考虑到惰性。相关问答:Does Haskell have tail-recursive optimization?foldl is tail recursive, so how come foldr runs faster than foldl?.

标签: haskell recursion lazy-evaluation


【解决方案1】:

让我们逐块考虑基于foldr 的解决方案是否可以短路。首先,(&&) 定义为 like this

(&&)                    :: Bool -> Bool -> Bool
True  && x              =  x
False && _              =  False

鉴于第二个子句,由于懒惰,如果第一个参数是 False(&&) 的第二个参数将被忽略——换句话说,它会短路。

接下来,this is foldr for lists

foldr            :: (a -> b -> b) -> b -> [a] -> b
foldr k z = go
          where
            go []     = z
            go (y:ys) = y `k` go ys

如果y `k` go ys 可以在不查看go ys 的情况下进行评估,则不会有递归调用,并且折叠作为一个整体将是快捷方式。

all1 中,二元运算是\x acc -> acc && p x。这对于我们的目的来说还不够好,因为将acc(对应于foldr 定义中的go ys)作为(&&) 的第一个短路参数会导致整个列表被消耗掉p x 原来是什么。不过,并非所有内容都丢失了:将参数交换为 (&&)...

all3 :: (a -> Bool) -> [a] -> Bool
all3 p xs = foldr (\x acc -> p x && acc) True xs

... 为我们提供所需的短路。

【讨论】:

    【解决方案2】:

    foldr定义如下:

    foldr k z = go
              where
                go []     = z
                go (y:ys) = y `k` go ys
    

    如果将此定义内联到all1,您会看到结果也是递归的。因此,它不是显式递归的,因为它隐藏了foldr内部的递归。

    foldr 变体更节省空间和时间,因为foldrlist fusion 的规则(删除中间列表的优化),all1 免费获得。

    要使短路工作,只需将acc && p x 更改为p x && acc。使用foldr,一旦得到False 结果,它将停止遍历列表。使用foldlfoldl',即使您的折叠功能短路,它也需要遍历列表的其余部分。

    总结:使用foldrfoldlfoldl' 或在您自己的函数中显式递归更有效。一个很好的简单测试是在 GHCi 中执行 +set :s,然后比较列表 (False:replicate 10000000 True) 上的性能。

    【讨论】:

    猜你喜欢
    • 2014-02-13
    • 2011-03-03
    • 1970-01-01
    • 1970-01-01
    • 2012-04-13
    • 2020-03-19
    • 2019-04-27
    • 2016-10-29
    • 1970-01-01
    相关资源
    最近更新 更多