在Haskell中组合ST和List monad

问题描述 投票:4回答:2

使用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)同构。

haskell state-monad non-deterministic st-monad
2个回答
4
投票

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

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

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


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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.