【问题标题】:Combine ST and List monads in Haskell在 Haskell 中结合 ST 和 List monad
【发布时间】:2019-01-23 06:07:34
【问题描述】:

使用StateT monad 转换器,我可以创建类型StateT s [] a,它与s -> [(a, s)] 同构。现在我更喜欢使用STT monad transformer,因为我想拥有多个不同类型的可变变量,并且希望能够根据早期计算的结果随意实例化它们。

但是,STT 的链接文档明确提到:

这个 monad 转换器不应该与可以包含多个答案的 monad 一起使用,比如 list monad。原因是状态令牌将在不同的答案中重复,这会导致坏事发生(例如失去参考透明度)。安全 monad 包括 monad State、Reader、Writer、Maybe 以及它们对应的 monad 转换器的组合。

那么我有什么选择呢?

完全清楚:

  • 我所追求的是非确定性。我希望能够分叉我的计算,为每个分支提供自己的整个状态的副本。
  • 我不太介意并行性,因为性能不是我最关心的问题。
  • 关注的是并发:不同的计算分支不应共享可变变量;相反,他们都应该使用自己的原始可变变量副本。

编辑: (编辑编辑:以下反例无效,因为 ListT 不应应用于非交换单子 STState。) 我开始意识到 STT monad 转换器的行为与 StateT 一样,本质上是不安全的。有了它,我们可以构建一个类型STT sloc (ListT (ST sglob)) a。这里,sglob 是全局状态的名称,而sloc 是本地状态的名称。* 现在我们可以使用全局状态在线程之间交换局部状态引用,从而有可能获得对未初始化变量的引用。

*为了比较,对应的StateT构造为StateT sloc (ListT (State sglob)) a,与sloc -> sglob -> ([(a, sloc)], sglob)同构。

【问题讨论】:

    标签: haskell state-monad non-deterministic st-monad


    【解决方案1】:

    你不会绕过StateT,因为对于这种不确定性的东西,编译器需要始终知道哪些“变量”需要被分支出来。当变量可能潜伏在STRefs 的任何地方时,这是不可能的。

    要仍然获得“不同类型的多个变量”,您需要将它们打包到合适的记录中,并将其用作单个实际状态变量。处理这样的状态对象似乎很尴尬?嗯,使用镜头访问“个体变量”并没有那么糟糕。

    {-# LANGUAGE TemplateHaskell #-}
    
    import Control.Lens
    import Data.Monoid
    
    import Control.Monad.Trans.State
    import Control.Monad.ListT
    import Control.Monad.Trans.Class
    import Control.Monad.IO.Class
    
    data Stobjs = Stobjs {
        _x :: Int
      , _y :: String
      }
    
    makeLenses ''Stobjs
    
    main = runListT . (`runStateT`Stobjs 10 "") $ do
       δx <- lift $ return 1 <> return 2 <> return 3
       xnow <- x <+= δx
       y .= show xnow
       if xnow > 11 then liftIO . putStrLn =<< use y
                    else lift mempty
    

    (输出12)。

    “能够随意实例化它们”有点棘手,因为只有通过更改状态对象才能添加变量,这意味着您将不再真正处于同一个 monad 中。 Lens 具有可以使用的 zooming 概念——将状态对象拆分为“范围”,并使用仅将部分变量定义为放大到该范围的计算。

    为了真正方便,您需要可以随意扩展的记录。我真的很喜欢 Nikita Volkovs record library approach,这似乎最近没有进一步发展。 Vinyl 也朝着这个方向发展,但我没有深入研究。

    将来,我们将有 OverloadedRecordFields extension 来帮助解决这类问题。

    【讨论】:

    • 谢谢。总结一下:你是说我应该自己实现一个穷人的STT。我想,如果我预先有一组要实例化变量的固定类型,我可以在我的状态下为每种类型设置一个映射,再加上一个允许我生成新键的计数器。
    • @dremodaris 是的,带有地图的东西可能是一个实用的解决方案,尽管它显然很不优雅并且破坏了编译时的安全性。
    • 不安全的方面肯定可以包含在 monad 转换器的内部吗? ST monad 本身甚至不是纯粹的。
    • 有趣的想法。是的,将弱类型映射包装到 看起来ST 但实际上是 StateT 的东西可能非常有用。
    【解决方案2】:

    不推荐这个答案,见the other one


    为了扩展您的想法,用弱类型变量映射包装 StateT,这看起来像以下内容:

    {-# LANGUAGE GADTs #-}
    
    import Unsafe.Coerce
    import Data.IntMap
    
    data WeakTyped where
       WeakTyped :: a -> WeakTyped
    
    newtype STT' m a = STT' { weakTypState :: StateT (IntMap WeakTyped) m a }
      deriving (Functor, Applicative, Monad)
    
    newtype STT'Ref a = STT'Ref { mapIndex :: Int }
    
    newSTTRef :: Monad m => a -> STT' m (STT'Ref a)
    newSTTRef x = STT' $ do
       i <- (+1) . maximum . keys <$> get
       modify $ insert i x
       return $ STT'Ref i
    
    readSTTRef :: Monad m => STT'Ref a -> STT' m a
    readSTTRef (STT'Ref i) = STT' $ do
       unsafeCoerce . (!i) <$> get
    

    我不相信这真的很聪明。这些STT'Refs 没有被Haskell 运行时正确处理,特别是状态变量不会被垃圾收集。因此,如果您在循环中运行使用newSTTRef 的操作,它实际上会在每次迭代中增加IntMap,而不会释放已经“超出范围”的变量(即没有任何引用指向他们)。

    也许可以为所有这些添加一个真正的垃圾收集器,但这会使它变得非常复杂。

    【讨论】:

    • 啊,我明白你的意思了。我正在考虑改用StateT (IntMap s) m a,其中sSTT' 的附加参数,所以如果你希望能够拥有BoolString 类型的变量,你需要使用单子STT' Bool (STT' String m)
    • 以下评论我不清楚:“如果您在某些本地范围内创建的变量比预期的多,那么您可能会泄漏不正确的索引”。您指的是我创建的引用比可用整数多的情况吗?
    • 是的,我没有考虑这么多,只是有一种鱼腥味的感觉。真正的问题是,如果您在循环中使用newSTTRef 执行操作,它只会不断将更多状态变量推入“堆栈”——没有垃圾收集。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-13
    • 2014-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-17
    相关资源
    最近更新 更多