我正在研究使用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
这样的事实来进行一些巧妙的优化。
那么,这种折衷方案总是存在吗?我是否对递归和折叠工作方式有所误解?
谢谢!
答案 0 :(得分:2)
foldr
的定义如下:
foldr k z = go
where
go [] = z
go (y:ys) = y `k` go ys
如果将此定义内联到all1
中,则会看到结果也是递归的。因此,它不是明确递归的,因为它将递归隐藏在foldr
内部。
foldr
变体既节省空间又节省时间,因为foldr
拥有list fusion(用于删除中间列表的优化)规则,all1
得到了免费。
要进行短路工作,只需将acc && p x
更改为p x && acc
。使用foldr
,它将在获得False
结果后立即停止浏览列表。使用foldl
或foldl'
,即使折叠功能发生短路,它仍然需要遍历列表的其余部分。
摘要:使用foldr
比foldl
,foldl'
或在您自己的函数中显式递归的效率更高。一个很好的简单测试是在GHCi中执行+set :s
,然后比较列表(False:replicate 10000000 True)
上的性能。
答案 1 :(得分:2)
让我们逐个考虑基于foldr
的解决方案是否会短路。首先,将like this定义为(&&)
:
(&&) :: Bool -> Bool -> Bool
True && x = x
False && _ = False
给出第二个子句,由于懒惰,如果第一个是(&&)
,则False
的第二个参数将被忽略-换句话说,它会短路。
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
的第一个短路参数传递go ys
(与foldr
定义中的(&&)
相对应)导致整个列表被消耗,而无论p x
到底是什么。不过,并不会丢失所有内容:将参数交换到(&&)
...
all3 :: (a -> Bool) -> [a] -> Bool
all3 p xs = foldr (\x acc -> p x && acc) True xs
...给我们所需的短路。