如何折叠懒惰类型列表([IO a])?

我不知道这是否是一个有效的术语'懒惰类型'。 但是,IO仍然很懒惰

import Control.Monad
import Data.List

result :: IO Double
result = foldl1' (liftM2 (+)) $ map return [1..10000000]

result' :: IO Double
result' = return $ foldl1' (+) [1..10000000]

result很慢并且使用大量内存,而不像result' 。 我该如何折叠[IO a]


result构造一个大的IO Double值,而不会评估任何中间结果,只有当总结果需要时才会发生,例如用于打印。 foldl'将中间结果评估为弱头标准形式,也就是最外层的构造函数或lambda。 由于(在GHC中), IO a具有构造函数IO ,因此折叠的中间结果具有这种形式

IO (some computation combined with another)

IO下的表达式在每一步都变得更加复杂。

为了避免这种情况,您不得不强制中间IO值,而且还要强制返回它们的值,

main :: IO ()
main = foldlM' (a -> fmap (a+)) 0 (map return [1.0 .. 10000000]) >>= print

foldlM' :: Monad m => (a -> b -> m a) -> a -> [b] -> m a
foldlM' foo a [] = return a
foldlM' foo a (b:bs) = do
    c <- foo a b
    c `seq` foldlM' foo c bs

适用于您的示例。


问题在于foldl'只会在每一步中将累加器减少到WHNF。 在这种情况下,累加器是一个IO动作,并且评估一个IO动作并不会强制内部的值,所以最终会得到一个典型的巨大thunk,直到最后才会被评估。

解决方案是使用比liftM2更严格的东西,例如:

result'' :: IO Double
result'' = foldl1' f $ map return [1..100000]
    where f mx my = do x <- mx; y <- my; return $! x + y

这是一个快速的基准:

import Control.Monad
import Data.List
import Criterion.Main

result :: IO Double
result = foldl1' (liftM2 (+)) $ map return [1..100000]

result' :: IO Double
result' = return $ foldl1' (+) [1..100000]

result'' :: IO Double
result'' = foldl1' f $ map return [1..100000]
  where f mx my = do x <- mx; y <- my; return $! x + y

main = defaultMain [ bench "result" $ whnfIO result
                   , bench "result'" $ whnfIO result'
                   , bench "result''" $ whnfIO result'' ]

结果:

[...]
benchmarking result
collecting 100 samples, 1 iterations each, in estimated 37.32438 s
mean: 136.3221 ms, lb 131.4504 ms, ub 140.8238 ms, ci 0.950
std dev: 23.92297 ms, lb 22.00429 ms, ub 25.53803 ms, ci 0.950

benchmarking result'
collecting 100 samples, 14 iterations each, in estimated 6.046951 s
mean: 4.349027 ms, lb 4.338121 ms, ub 4.367363 ms, ci 0.950
std dev: 70.96316 us, lb 49.01322 us, ub 113.0399 us, ci 0.950

benchmarking result''
collecting 100 samples, 2 iterations each, in estimated 8.131099 s
mean: 41.89589 ms, lb 40.67513 ms, ub 43.52798 ms, ci 0.950
std dev: 7.194770 ms, lb 5.758892 ms, ub 8.529327 ms, ci 0.950

正如你所看到的,这仍然比纯代码慢,但不是那么多。


根据Daniel的回答,这个变体允许直接实现foldl1'的模拟:

result'' :: IO Double
result'' = foldlM1' (+) (map return [1.0 .. 100000000])

foldlM' :: Monad m => (a -> b -> a) -> m a -> [m b] -> m a
foldlM' foo ma [] = ma
foldlM' foo ma (mb:mbs) = do
    c <- (liftM2 foo) ma mb
    c `seq` foldlM' foo (return c) mbs

foldlM1' foo (x:xs) = foldlM' foo x xs
foldlM1' _ []       = error "Empty list for foldlM1'"
链接地址: http://www.djcxy.com/p/42987.html

上一篇: How to fold lists of lazy types ([IO a])?

下一篇: Does Haskell's 'evaluate' reduce to normal or WHNF?