您实际上并不需要了解如何State#
实施。您只需将其视为通过计算线程传递的令牌,以确保操作的特定执行顺序,ST
否则这些操作可能会被优化掉。
在STT s []
monad 中,您可以将列表操作视为生成一棵可能的计算树,并在叶子上给出最终答案。在每个分支点,State#
令牌都会被拆分。所以,粗略地说:
- 在从根到叶的特定路径中,单个
State#
令牌贯穿整个路径,因此当需要答案时,所有 ST 操作将按顺序执行
- 对于两条路径,它们共有的树部分中的 ST 操作(在拆分之前)是安全的,并且以您期望的方式在两条路径之间正确“共享”
- 两条路径拆分后,两个独立分支中动作的相对顺序未指定
我相信还有一个进一步的保证,尽管这有点难以推理:
如果在最终的答案列表(即由 生成的列表runSTT
)中,您在 index 处强制使用单个答案k
- 或者,实际上,我认为如果您只是强制证明在 index 处有答案的列表构造函数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
当使用(answer is [3,3,3]
) 与-O2
(answer is )编译时,在 GHC 8.4.3 下会产生不同的答案[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
.