使用以下代码:
(lazy_test.hs
)
-- Testing lazy evaluation of monadically constructed lists, using State.
import Control.Monad.State
nMax = 5
foo :: Int -> State [Int] Bool
foo n = do
modify $ \st -> n : st
return (n `mod` 2 == 1)
main :: IO ()
main = do
let ress = for [0..nMax] $ \n -> runState (foo n) []
sts = map snd $ dropWhile (not . fst) ress
print $ head sts
for = flip map
我可以设置nMax
为5或50,000,000,并且得到大致相同的运行时间:
nMax = 5
:
$ stack ghc lazy_test.hs
[1 of 1] Compiling Main ( lazy_test.hs, lazy_test.o )
Linking lazy_test ...
$ time ./lazy_test
[1]
real 0m0.019s
user 0m0.002s
sys 0m0.006s
nMax = 50,000,000
:
$ stack ghc lazy_test.hs
[1 of 1] Compiling Main ( lazy_test.hs, lazy_test.o )
Linking lazy_test ...
$ time ./lazy_test
[1]
real 0m0.020s
user 0m0.002s
sys 0m0.005s
鉴于我对惰性评估机制的理解,这正是我所期望的。
但是,如果我从切换State
到StateT
:
(lazy_test2.hs
)
-- Testing lazy evaluation of monadically constructed lists, using StateT.
import Control.Monad.State
nMax = 5
foo :: Int -> StateT [Int] IO Bool
foo n = do
modify $ \st -> n : st
return (n `mod` 2 == 1)
main :: IO ()
main = do
ress <- forM [0..nMax] $ \n -> runStateT (foo n) []
let sts = map snd $ dropWhile (not . fst) ress
print $ head sts
for = flip map
然后我看到各个运行时间之间的极端差异:
nMax = 5
:
$ stack ghc lazy_test2.hs
[1 of 1] Compiling Main ( lazy_test2.hs, lazy_test2.o )
Linking lazy_test2 ...
$ time ./lazy_test2
[1]
real 0m0.019s
user 0m0.002s
sys 0m0.004s
nMax = 50,000,000
:
$ stack ghc lazy_test2.hs
[1 of 1] Compiling Main ( lazy_test2.hs, lazy_test2.o )
Linking lazy_test2 ...
$ time ./lazy_test2
[1]
real 0m29.758s
user 0m25.488s
sys 0m4.231s
我以为那是因为当我切换到StateT
基于-的实现时,我对单子构造的列表失去了懒惰的评估。
那是对的吗?
我是否可以恢复对单子构造列表的惰性评估,同时保持StateT
基于-的实现?
KA Buhr的另一个答案解释了为什么State
vsStateT
不是相关因素(IO
is),还指出了示例的构造方式是奇怪的(因为该State(T)
部分实际上并未使用,因为每个数字都使用新状态[]
)。但是除了这些要点之外,我不确定我会说“失去对一元构造列表的惰性评估”,因为如果我们理解诸如“惰性评估=仅在需要时进行评估”之类的东西,那么foo
确实确实需要在每个元素上运行为了执行所有效果,请不要在输入列表上输入,因此懒惰的评估不会“丢失”。您正在得到想要的东西。(正好foo
不会执行任何IO,如果编译器/ GHC可以在此基础上对其进行优化,也许其他人可以发表评论,但是您可以在这里轻松地了解GHC为什么会做幼稚的事情。)
这是Haskell中常见的常见问题。有各种库(最有名的这些都是streaming
,pipes
,conduit
),它给你解决问题流(基本清单),这是懒惰的影响太大。如果我以流式方式重新创建您的示例,
import Data.Function ((&))
import Control.Monad.State
import Streaming
import qualified Streaming.Prelude as S
foo :: Int -> StateT [Int] IO Bool
foo n =
(n `mod` 2 == 1) <$ modify (n:)
nMax :: Int
nMax = 5000000
main :: IO ()
main = do
mHead <- S.head_ $ S.each [0..nMax]
& S.mapM (flip runStateT [] . foo)
& S.dropWhile (not . fst)
print $ snd <$> mHead
然后这两个版本几乎都可以立即运行。为了使区别更加明显,请想象foo
也称为print "hi"
。然后,对效果不满意的流媒体版本将仅打印两次,而原始版本将同时打印nMax
两次。由于它们在效果上比较懒惰,因此不需要遍历整个列表即可短路并尽早完成。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句