从State切换到StateT后,如何恢复对单子构造的列表的惰性计算?

dbanas

使用以下代码:

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

鉴于我对惰性评估机制的理解,这正是我所期望的。

但是,如果我从切换StateStateT

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基于-的实现时,我对单子构造的列表失去了懒惰的评估

  1. 那是对的吗?

  2. 我是否可以恢复对单子构造列表的惰性评估,同时保持StateT基于-的实现?

月鹅

KA Buhr的另一个答案解释了为什么StatevsStateT不是相关因素(IOis),还指出了示例的构造方式是奇怪的(因为该State(T)部分实际上并未使用,因为每个数字都使用新状态[])。但是除了这些要点之外,我不确定我会说“失去对一元构造列表的惰性评估”,因为如果我们理解诸如“惰性评估=仅在需要时进行评估”之类的东西,那么foo确实确实需要在每个元素上运行为了执行所有效果,请不要在输入列表上输入,因此懒惰的评估不会“丢失”。您正在得到想要的东西。(正好foo 不会执行任何IO,如果编译器/ GHC可以在此基础上对其进行优化,也许其他人可以发表评论,但是您可以在这里轻松地了解GHC为什么会做幼稚的事情。)

这是Haskell中常见的常见问题。有各种库(最有名的这些都是streamingpipesconduit),它给你解决问题(基本清单),这是懒惰的影响太大。如果我以流式方式重新创建您的示例,

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] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章