在Haskell中递归或折叠

时间:2019-03-27 23:50:44

标签: haskell recursion lazy-evaluation

我正在研究使用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这样的事实来进行一些巧妙的优化。

那么,这种折衷方案总是存在吗?我是否对递归和折叠工作方式有所误解?

谢谢!

2 个答案:

答案 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结果后立即停止浏览列表。使用foldlfoldl',即使折叠功能发生短路,它仍然需要遍历列表的其余部分。

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

答案 1 :(得分:2)

让我们逐个考虑基于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的第一个短路参数传递go ys(与foldr定义中的(&&)相对应)导致整个列表被消耗,而无论p x到底是什么。不过,并不会丢失所有内容:将参数交换到(&&) ...

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

...给我们所需的短路。