You don't really need to understand how State#
is implemented. You just need to think of it as a token that's passed through a thread of computations to ensure a certain execution order of ST
actions which might otherwise be optimized away.
In an STT s []
monad, you can think of the list actions as producing a tree of possible computations with the final answers at the leaves. At each branching point, the State#
token is split. So, roughly speaking:
- within a particular path from root to leaf, a single
State#
token is threaded through the whole path, so all ST actions will be executed in order when the answer is demanded
- for two paths, ST actions within the portion of the tree they have in common (before splitting) are safe and properly "shared" between the two paths in the way you'd expect
- after two paths split, the relative ordering of the actions in the two independent branches is unspecified
There's a further guarantee, I believe, though it's a little hard to reason about:
If, in the final list of answers (i.e., the list produced by runSTT
), you force the single answer at index k
-- or, actually, I think if you just force the list constructor that proves there is an answer at index k
-- then all actions in a depth-first traversal of the tree up to that answer will have been performed. The catch is that other actions in the tree may have been executed as well.
As an example, the following program:
{-# 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
produces different answers under GHC 8.4.3 when compiled with -O0
(answer is [3,3,3]
) versus -O2
(answer is [3,7,15]
).
In its (simple) tree of computations:
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)
we can reason that when the first value is demanded, the write/read/read/write actions in the left branch have been executed. (In this case, I think that the write and read on the middle branch have also been executed, as explained below, but I'm a little unsure.)
When the second value is demanded, we know that all the actions in the left branch have been executed in order, and all the actions in the middle branch have been executed in order, but we don't know the relative order between those branches. They may have been executed fully sequentially (giving the answer 3
), or they may have been interleaved so that the final write on the left branch fell between the two reads on the right branch (giving the answer 1 + 2*3 = 7
.
STT
is safe without understandingState#
. Specifically: is theState#
used affinely? Then it is safe. Otherwise, not safe. You do not need to know the internal implementation details ofState#
to make this determination. – Daniel WagnerrealWorld# :: State# RealWorld
is certainly not used affinely: it is used every time someone invokesrunST
. (I do not intend to use theState#
affinely, and would like to understand what happens if I don't.) – dremodarisSTT s []
monad. – dremodarisST
is to use the type system to guarantee a computation that uses mutable state is self-contained, so it can be treated as pure from the perspective of the surrounding context. Think of it as “type-safe, restrictedIO
”, nothing more. The “state token” does not include any actual state, it is merely an internal implementation detail used byIO
andST
to ensure ordering. You cannot safely useSTT s []
, as the documentation states. If you want to be able to fork the state, you need to useStateT
. – Alexis King