[英]Recursion or fold in haskell
我正在学习一些使用 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
一次,它的存在。
那么,这种妥协是否一直存在? 我是否误解了递归和折叠的工作方式?
foldr
定义如下:
foldr k z = go
where
go [] = z
go (y:ys) = y `k` go ys
如果您将此定义内联到all1
,您将看到结果也是递归的。 因此,它只是没有明确递归,因为它隐藏了foldr
内部的递归。
foldr
变体既节省空间又节省时间,因为foldr
具有列表融合规则(一种删除中间列表的优化), all1
可以免费获得。
要使短路工作,只需将acc && px
更改为px && acc
。 使用foldr
,一旦得到False
结果,它就会停止遍历列表。 使用foldl
或foldl'
,即使您的折叠功能短路,它仍然需要遍历列表的其余部分。
总结:使用foldr
比foldl
、 foldl'
或在您自己的函数中显式递归更有效。 一个很好的简单测试是在 GHCi 中执行+set :s
,然后比较列表上的性能(False:replicate 10000000 True)
。
让我们一砖一瓦地考虑基于foldr
的解决方案是否可以短路。 首先, (&&)
定义如下:
(&&) :: Bool -> Bool -> Bool
True && x = x
False && _ = False
考虑到第二个子句,并且由于懒惰,如果第一个参数为False
,则(&&)
的第二个参数将被忽略——换句话说,它短路了。
接下来, 这是列表的文件foldr
:
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 && px
。 这对于我们的目的来说还不够好,因为将acc
(对应于foldr
定义中的go ys
)作为第一个,短路, (&&)
参数导致整个列表被消耗,而不管px
结果是什么成为。 但是,并非所有内容都丢失了:将参数交换为(&&)
...
all3 :: (a -> Bool) -> [a] -> Bool
all3 p xs = foldr (\x acc -> p x && acc) True xs
... 为我们提供了所需的短路。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.