使用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)
同构。
你不会绕过StateT
,因为对于这种非确定性的东西,编译器需要知道哪些“变量”需要扩展。当变量可能潜伏在STRef
s的任何地方时,这是不可能的。
要仍然获得“不同类型的多个变量”,您需要将它们打包在合适的记录中,并将其用作单个实际状态变量。处理这样的状态对象似乎很尴尬?嗯,使用镜头访问“个体变量”并没有那么糟糕。
{-# 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,将有助于这种东西。
这个答案不推荐,请参阅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'Ref
s,特别是状态变量不会被垃圾回收。因此,如果你运行一个在循环中使用newSTTRef
的动作,它实际上会在每次迭代中增长IntMap
,而不会释放已经“超出范围”的变量(即没有任何引用指向它们) 。
可能会为所有这些添加一个实际的垃圾收集器,但这会使它变得非常复杂。