如何使用 foldr/foldl 定义 foldM(如果可能)?
Posted
技术标签:
【中文标题】如何使用 foldr/foldl 定义 foldM(如果可能)?【英文标题】:How to define foldM using foldr/foldl (if it is possible)? 【发布时间】:2012-10-14 09:38:51 【问题描述】:我想制作一个通用函数,可以折叠各种输入(请参阅Making a single function work on lists, ByteStrings and Texts (and perhaps other similar representations))。作为one answer suggested,ListLike 就是为了这个。它的FoldableLL 类为任何可折叠的东西定义了一个抽象。但是,我需要一个单子折叠。所以我需要根据foldl
/foldr
来定义foldM
。
到目前为止,我的尝试都失败了。我试图定义
foldM'' :: (Monad m, LL.FoldableLL full a) => (b -> a -> m b) -> b -> full -> m b
foldM'' f z = LL.foldl (\acc x -> acc >>= (`f` x)) (return z)
但它在大量输入时会耗尽内存 - 它会构建一个大型未评估的计算树。例如,如果我将一个大文本文件传递给
main :: IO ()
main = getContents >>= foldM'' idx 0 >> return ()
where
-- print the current index if 'a' is found
idx !i 'a' = print i >> return (i + 1)
idx !i _ = return (i + 1)
它会耗尽所有内存并失败。
我感觉问题在于一元计算的组合顺序错误——比如((... >>= ...) >>= ...)
而不是(... >>= (... >>= ...))
,但到目前为止我还没有找到解决方法。
解决方法:由于ListLike
暴露了mapM_
,我通过将累加器包装到状态单子中,在ListLike
s 上构造了foldM
:
modifyT :: (Monad m) => (s -> m s) -> StateT s m ()
modifyT f = get >>= \x -> lift (f x) >>= put
foldLLM :: (LL.ListLike full a, Monad m) => (b -> a -> m b) -> b -> full -> m b
foldLLM f z c = execStateT (LL.mapM_ (\x -> modifyT (\b -> f b x)) c) z
虽然这适用于大型数据集,但它不是很好。如果可以在仅 FoldableLL
(没有 mapM_
)的数据上定义它,它并没有回答最初的问题。
【问题讨论】:
为什么不使用foldM
from Control.Monad
?还要使用严格版本foldl'
而不是foldl
。
@Satvik 因为我不仅需要它来定义列表,还需要定义任何FoldableLL,它只暴露不同种类的纯折叠,而不是单子折叠。我的错,我没有在帖子中说清楚 - 我已经更正了类型签名。
要以相反的顺序组合计算,请使用foldr
而不是foldl
。
@dave4420 我也试过了,但是没用。
【参考方案1】:
所以目标是使用foldr
或foldl
重新实现foldM
。应该是哪一个?我们希望延迟处理输入并允许无限列表,这排除了foldl
。所以foldr
会是这样吗?
这是标准库中foldM
的定义。
foldM :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a
foldM _ a [] = return a
foldM f a (x:xs) = f a x >>= \fax -> foldM f fax xs
关于foldr
要记住的一点是,它的参数只是替换了列表中的[]
和:
(ListLike
对此进行了抽象,但它仍然作为指导原则)。
那么[]
应该替换成什么呢?显然是return a
。但是a
是从哪里来的呢?传递给foldM
的不是最初的a
——如果列表不为空,当foldr
到达列表末尾时,累加器应该已经改变。因此,我们将[]
替换为一个接受累加器并在底层monad 中返回它的函数:\a -> return a
(或简称return
)。这也给出了foldr
将计算的事物的类型:a -> m a
。
我们应该用什么替换:
?它需要是一个函数b -> (a -> m a) -> (a -> m a)
,获取列表的第一个元素、处理后的尾部(当然是懒惰的)和当前的累加器。我们可以从上面的代码中得到提示:它将是\x rest a -> f a x >>= rest
。所以我们对foldM
的实现将是(在上面的代码中调整类型变量以匹配它们):
foldM'' :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a
foldM'' f z list = foldr (\x rest a -> f a x >>= rest) return list z
事实上,现在您的程序可以消耗任意大的输入,并在执行过程中吐出结果。
我们甚至可以通过归纳证明定义在语义上是相等的(尽管我们可能应该进行共归纳或取归纳来满足无限列表的需要)。
我们想展示
foldM f a xs = foldM'' f a xs
所有xs :: [b]
。对于xs = []
,我们有
foldM f a []
≡ return a -- definition of foldM
≡ foldr (\x rest a -> f a x >>= rest) return [] a -- definition of foldr
≡ foldM'' f a [] -- definition of foldM''
并且,假设我们为xs
提供了它,我们为x:xs
显示它:
foldM f a (x:xs)
≡ f a x >>= \fax -> foldM f fax xs --definition of foldM
≡ f a x >>= \fax -> foldM'' f fax xs -- induction hypothesis
≡ f a x >>= \fax -> foldr (\x rest a -> f a x >>= rest) return xs fax -- definition of foldM''
≡ f a x >>= foldr (\x rest a -> f a x >>= rest) return xs -- eta expansion
≡ foldr (\x rest a -> f a x >>= rest) return (x:xs) -- definition of foldr
≡ foldM'' f a (x:xs) -- definition of foldM''
当然,这种等式推理不会告诉您有关您感兴趣的性能属性的任何信息。
【讨论】:
我想要foldM
在列表中所做的一切。所以左折叠,无限列表不行。我会尝试用更有意义的东西替换idx
。
foldM
确实适用于无限列表,因此单子动作是左折叠的,而累加器是右折叠的。或者类似的东西:-)
@PetrPudlák:列表的左折叠也可以写成右折叠:haskell.org/haskellwiki/Foldl_as_foldr以上是关于如何使用 foldr/foldl 定义 foldM(如果可能)?的主要内容,如果未能解决你的问题,请参考以下文章