但是,STT
的文档说:
此 monad 转换器不应与可以包含多个答案的 monad 一起使用,例如列表 monad。原因是状态令牌将在不同的答案中重复,这会导致坏事发生(例如失去引用透明度(。安全单子包括单子状态、读取器、写入器、可能及其相应单子转换器的组合。
我希望能够自己判断STT
monad的某种使用是否安全。特别是,我想了解与列表monad的相互作用。我知道STT sloc (ListT (ST sglob)) a
不安全,但STT sloc []
呢?
我发现(至少在GHC中(,STT
最终是使用MuteVar#
,State#
,realWorld#
等神奇的结构来实现的。是否有关于这些对象行为方式的准确文档?
这与我前面的一个问题密切相关。
你真的不需要了解State#
是如何实现的。 您只需要将其视为通过计算线程传递的令牌,以确保ST
操作的特定执行顺序,否则这些操作可能会被优化掉。
在STT s []
monad 中,您可以将列表操作视为生成一个可能的计算树,最终答案位于叶子处。 在每个分支点,拆分State#
令牌。 所以,粗略地说:
- 在从根到叶的特定路径中,单个
State#
令牌贯穿整个路径,因此当需要答案时,所有 ST 操作将按顺序执行
对于两条路径 - ,它们共同的树部分中的 ST 操作(拆分之前(是安全的,并且以您期望的方式在两条路径之间正确"共享">
- 拆分两条路径后,未指定两个独立分支中操作的相对顺序
我相信还有进一步的保证,尽管有点难以推理:
如果在最终的答案列表(即runSTT
生成的列表(中,你强制索引k
处的单个答案 - 或者,实际上,我认为如果你只是强制列表构造函数证明在索引k
有一个答案 - 那么树的深度优先遍历中的所有操作都将被执行,直到该答案。 问题是树中的其他操作也可能已被执行。
例如,以下程序:
{-# OPTIONS_GHC -Wall #-}
import Control.Monad.Trans
import Control.Monad.ST.Trans
type M s = STT s []
foo :: STRef s Int -> M s Int
foo r = do
_ <- lift [1::Int,2,3]
writeSTRef r 1
n1 <- readSTRef r
n2 <- readSTRef r
let f = n1 + n2*2
writeSTRef r f
return f
main :: IO ()
main = print $ runSTT $ foo =<< newSTRef 9999
使用-O0
编译时,GHC 8.4.3 下产生不同的答案(答案为[3,3,3]
(与-O2
(答案为[3,7,15]
(。
在其(简单(计算树中:
root
/ |
1 2 3 _ <- lift [1,2,3]
/ |
wr wr wr writeSTRef r 1
| | |
rd rd rd n1 <- readSTRef r
| | |
rd rd rd n2 <- readSTRef r
| | |
wr wr wr writeSTRef r (n1 + n2*2)
| | |
f f f return (n1 + n2*2)
我们可以推断,当需要第一个值时,左分支中的写入/读取/读取/写入操作已被执行。 (在这种情况下,我认为中间分支上的写入和读取也已执行,如下所述,但我有点不确定。
当需要第二个值时,我们知道左分支中的所有操作都已按顺序执行,中间分支中的所有操作已按顺序执行,但我们不知道这些分支之间的相对顺序。 它们可能是完全按顺序执行的(给出答案3
(,或者它们可能是交错的,以便左分支上的最终写入落在右分支上的两个读取之间(给出答案1 + 2*3 = 7
(。