首页 文章

在Haskell中组合ST和List monad

提问于
浏览
4

使用 StateT monad变换器,我可以创建类型 StateT s [] a ,它与 s -> [(a, s)] 同构 . 现在我宁愿使用STT monad transformer,因为我希望有多个不同类型的可变变量,并且希望能够随意实例化它们,具体取决于早期计算的结果 .

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

此monad变换器不应与包含多个答案的monad一起使用,例如list monad . 原因是状态令牌将在不同的答案中重复,这会导致坏事发生(例如参考透明度丢失) . 安全monad包括monads State,Reader,Writer,Maybe和他们相应的monad变换器的组合 .

那么我的选择是什么?

要完全清楚:

  • 我所追求的是非决定论 . 我希望能够分叉我的计算,为每个分支提供整个状态的副本 .

  • 我对并行性并不在意,因为性能不是我最关心的问题 .

  • 我不追求的是并发:不同的计算分支不应该共享可变变量;相反,它们应该都在自己的原始可变变量的副本上工作 .

编辑:我已经意识到 STT monad变换器的行为与 StateT 的行为本质上是不安全的 . 有了它,我们可以构建一个 STT sloc (ListT (ST sglob)) a 类型 . 这里, sglob 是全局状态的名称,而 sloc 是本地状态的名称 . *现在我们可以使用全局状态在线程之间交换本地状态引用,从而可能获得对未初始化变量的引用 .

*为了进行比较,相应的 StateT 构造是 StateT sloc (ListT (State sglob)) a ,它与 sloc -> sglob -> ([(a, sloc)], sglob) 同构 .

2 回答

  • 2

    你不会绕过 StateT ,因为对于这种非确定性的东西,编译器需要知道哪些“变量”需要分支出来 . 当变量可能潜伏为 STRef 时,这是不可能的 .

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

    {-# 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中 . 镜头具有zooming的概念,可以使用 - 将状态对象拆分为“范围”并使用计算,其中只有一些变量被定义为放大到该范围 .

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

    在未来,我们将拥有OverloadedRecordFields extension,这将有助于这种东西 .

  • 4

    不推荐这个答案,请参阅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'Ref 不是垃圾收集的 . 因此,如果您在循环中运行使用 newSTTRef 的操作,它将在每次迭代中实际增长 IntMap ,而不会释放已经“超出范围”的变量(即,没有任何引用指向它们) .

    可能会为所有这些添加一个实际的垃圾收集器,但这会使它变得非常复杂 .

相关问题