假设我必须实现一个功能
f :: Foo -> ReaderT Bar IO Baz
我必须传递给消费者(即我会打电话给
c f
)
其中 Foo
/Bar
/Baz
被强加给函数的使用者,并且使用者将使用不同的 Foo
重复调用该函数。
我有机会在连续调用 f
之间保持一些 local
在某种程度上,我已经通过
IORef
实现了这一点,通过更改 f
的签名
f :: IORef S -> Foo -> ReaderT Client IO a
并将其部分应用到我事先初始化的某些状态:
s <- initState :: IO (IORef S)
c $ f s
这样,完全应用的
f
就可以通过 s
和一些 IO
pdate 函数来改变 atomicModifyIORef'
monad 中的 u
状态:
f s x = do
liftIO $ atomicModifyIORef' s u
-- ...
但是,上面的解决方案对我来说很自然,因为
s
tate 确实是一个 global 可变状态,它不仅可以通过调用 f
进行修改,还可以通过同时运行的程序的其他部分进行修改与 f
。
但现在我需要
f
来拥有一些 private 状态,这些状态不应该被其他任何人修改。这让我想到了 StateT
变压器,但我真的不知道我可以在这种情况下应用它。
当前的实际用例是我想要在实现通知服务器的上下文中使用有状态的
notify
函数。请参阅here了解实现的玩具示例。在这种情况下,Foo -> ReaderT Bar IO Baz
实际上是MethodCall -> ReaderT Client IO Reply
(ReaderT Client IO a
是DBusR a
)。
正如你所看到的,无论我对
notify
做什么,我最终都必须将 MethodCall -> DBusR Reply
函数传递给 export
client
,并且该函数将被多次调用,并且预计每次都会返回,所以在我看来,保持状态的唯一方法是闭包,即我必须在 notify
之前提供 MethodCall
另一个参数,并将其部分应用于初始状态,正如我上面所解释的。每次传递 MethodCall
时改变该状态的唯一方法是让第一个附加参数成为真正可变的状态,例如我上面提到的IORef
。
是这个吗?
想象
f
的签名是:
f :: Foo -> Reader Bar Baz
并且您以某种方式在
StateT
中“使用了 f
”,通过对 f
的一系列调用来线程化本地状态。这意味着使用相同的 f
参数对 Foo
进行两次连续调用,结果读取器使用相同的 Bar
参数运行:
let baz1 = runReader (f foo) bar
baz2 = runReader (f foo) bar
in ...
可能会导致两种不同的
baz1
和 baz2
结果,具体取决于当地状态的变化,对吧?换句话说,这将违反引用透明度。
所以,你可以通过f :: Foo -> ReaderT Bar IO Baz
携带状态的
唯一原因是因为你有它的基本单子
IO
可用,所以你想出的任何解决方案都必须使用IO
效果来维持状态.
但是,在
StateT
的定义中使用 f
非常容易,同时实际上将状态维护为 IORef
私有的 f
。如果您使用 f
编写核心 StateT
逻辑:
f :: Foo -> StateT S (ReaderT Client IO) a
f foo = do ...
你可以建立一种
f
工厂:
f_factory :: IO (Foo -> ReaderT Client IO a)
f_factory = do
sref <- newIORef initS
pure $ \foo -> do
s <- liftIO $ readIORef sref
(r, s') <- runStateT (f foo) s
liftIO $ writeIORef sref s'
pure r
在IO中,你可以这样写:
f_with_state <- f_factory
c f_with_state -- pass to consumer
请注意,由于
sref
是在 f_factory
内创建的,因此它包含在生成的闭包 f_with_state
中,但在其他地方不可用。并且,在 f
的核心定义中,您可以只使用 get
、put
、modify
等,而不必担心实际的 f_with_state
函数正在恢复并保存状态私人 sref
IORef。