【问题标题】:Haskell: How lazy is the lazy `Control.Monad.ST.Lazy` monad?Haskell:懒惰的 `Control.Monad.ST.Lazy` monad 有多懒惰?
【发布时间】:2014-06-06 01:49:00
【问题描述】:

我一直在尝试严格和懒惰的ST monads,我并不清楚每个人的懒惰程度。 例如,使用惰性 Control.Monad.State.Lazy monad 我们可以这样写:

main = print $ (flip evalState) "a" $ do
    forever $ put "b"
    put "c"
    get

这工作正常并输出"c"。双重的,严格的Control.Monad.State.Strict 变体的相同代码将永远运行put "b",然后挂起。

直觉上,我希望ST monads 具有相同的二元性。也就是说,给定代码:

main = print $ S.runST $ do
        r <- newSTRef "a"
        forever $ writeSTRef r "b"
        writeSTRef r "c"
        readSTRef r

Control.Monad.ST.Lazy 应该输出"c",而Control.Monad.ST.Strict 应该挂起。但是,它们都无限循环。我相信这是有正当理由的,例如:向后阅读,在调用最后一个writeSTRef 时尚未分配引用r。但不知何故,感觉我们可以做得更好。

【问题讨论】:

  • 您可以使用Control.Monad.Lazy.Unsafe.unsafeInterleaveST $ forever $ writeSTRef r "b" 来获得您正在寻找的懒惰。不过,在不了解您的问题的情况下,我不能说这样做是否真的安全。
  • 是的,这就是我找到的解决方案。 @luqui 也在 [stackoverflow.com/questions/24068399/… 线程上对此发表评论。 forever 有点夸张,因为您似乎至少需要追溯修改的历史(而不是在我希望的相同参考上评估修改的历史)到创建点。
  • 哦,我应该指出unsafeInterleaveST 意味着推迟对其参数的评估,直到需要结果值。这意味着如果您忽略 unsafeInterleaveST 的结果(如我的评论中所述),您也可以删除该行。 unsafeInterleave* 函数真正用于创建列表等辅助数据。

标签: haskell


【解决方案1】:

懒惰的Control.Monad.ST.Lazy monad 有多懒?

令人惊讶的是,它完全是懒惰的。但Data.STRef.Lazy 不是。

ST.Lazy 懒惰

让我们先关注另一个例子:

import qualified Control.Monad.ST as S
import qualified Control.Monad.ST.Lazy as L

squared :: Monad m => m [Integer]
squared = mapM (return . (^2)) [1..]

ok, oops :: [Integer]
ok   = L.runST squared
oops = S.runST squared

即使okoops 应该做同样的事情,我们也只能得到ok 的元素。如果我们尝试使用head oops,我们会失败。但是对于ok,我们可以任意取多个元素。

或者,要将它们与非单子平方列表进行比较,它们的行为类似于:

ok, oops :: [Integer]
ok'   = map (^2) [1..]
oops' = let x = map (^2) [1..] in force x -- Control.DeepSeq.force

这是因为严格版本会评估所有状态操作,即使它们不是我们的结果所必需的。另一方面,惰性版本会延迟操作:

这个模块提供了一个与 Control.Monad.ST 相同的接口,除了 monad 延迟状态操作的评估,直到需要一个依赖于它们的值。

readSTRef 呢?

现在让我们再次关注您的示例。请注意,我们可以使用更简单的代码获得无限循环:

main = print $ L.runST $ do
    forever $ return ()
    r <- newSTRef "a"
    readSTRef r

如果我们在末尾添加一个额外的return……

main = print $ L.runST $ do
    forever $ return ()
    r <- newSTRef "a"
    readSTRef r
    return "a"

……一切都很好。所以显然newSTRefreadSTRef 中有一些严格的东西。让我们看看他们的implementation

import qualified Data.STRef as ST

newSTRef        = strictToLazyST . ST.newSTRef
readSTRef       = strictToLazyST . ST.readSTRef
writeSTRef  r a = strictToLazyST (ST.writeSTRef r a)

还有罪魁祸首。 Data.STRef.Lazy 实际上是通过Data.STRef 实现的,这意味着Control.Monad.ST.StrictstrictToLazyST 只隐藏了这个细节:

strictToLazyST :: ST.ST s a -> ST s a
strictToLazyST m = ST $ \s ->

将严格的 ST 计算转换为惰性计算。传递给strictToLazyST的严格状态线程直到要求它返回的惰性状态线程的结果才被执行。

现在让我们把事情放在一起:

  • main 中,我们想要print 由惰性ST 计算给出的值
  • 惰性ST 计算的值由惰性readSTRef 给出
  • 惰性readSTRef 实际上是作为一个惰性包装器实现的,它围绕着严格的readSTRef
  • 严格的readSTRef 评估状态就好像它是一个严格的状态
  • forever $ return () 的严苛评价咬我们

所以现在的ST.Lazy 已经够懒了。 Data.STRef.Lazy太严格了。只要Data.STRef.Lazy是基于strictToLazyST的,这种行为就会一直存在。

【讨论】:

  • 感谢您抽出宝贵的时间,它检查出来了。从之前的讨论来看,Data.STRef 似乎更严格是有原因的。
  • 我什至冒着风险说它不可能完全懒惰,因为不像普通的旧 State monad 分配由 runState 执行,因此在 a State.put可以忽略,它需要为每个newSTRef动态分配新空间。然而,我想到了一种可能的优化:对于不同类型的STRefs,newSTRefreadSTRefwriteSTRef 操作可以延迟交错。
  • @hpacheco:关于State:它本身并没有被忽略,而是使用了一种无可辩驳的模式,这使它足够懒惰,这样我们就可以产生这种效果。另一方面,STRef 上的操作会更改状态,RealWorld 或执行线程s。这在read|new|write|modifyMutVar 中得到了深入的实施。 real 懒惰的STRef 需要收集动作,然后在解析完所有动作后折叠它们。如果没有unsafeInterleaveST,这是否可能,我不知道。我对 Haskell 还是很陌生 :)。
猜你喜欢
  • 2014-03-23
  • 2015-09-06
  • 2015-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多