How lazy is the lazy Control.Monad.ST.Lazy
monad?
Surprisingly, it is perfectly lazy. But Data.STRef.Lazy
isn't.
ST.Lazy
is lazy
Lets focus on another example for a second:
import qualified Control.Monad.ST as S
import qualified Control.Monad.ST.Lazy as L
squared :: Monad m => m [Integer]
squared = mapM (return . (^2)) [1..]
ok, oops :: [Integer]
ok = L.runST squared
oops = S.runST squared
Even though ok
and oops
should do the same, we can get only the elements of ok
. If we would try to use head oops
, we would fail. However, concerning ok
, we can take arbitrarily many elements.
Or, to compare them to the non-monadic squared list, they behave like:
ok, oops :: [Integer]
ok' = map (^2) [1..]
oops' = let x = map (^2) [1..] in force x -- Control.DeepSeq.force
That's because the strict version evaluates all state operations, even though they're not required for our result. On the other hand, the lazy version delays the operations:
This module presents an identical interface to Control.Monad.ST, except that the monad delays evaluation of state operations until a value depending on them is required.
What about readSTRef
?
Now lets focus again on your example. Note that we can get an infinite loop with even simpler code:
main = print $ L.runST $ do
forever $ return ()
r <- newSTRef "a"
readSTRef r
If we add an additional return
at the end …
main = print $ L.runST $ do
forever $ return ()
r <- newSTRef "a"
readSTRef r
return "a"
… everything is fine. So apparently there's something strict in newSTRef
or readSTRef
. Lets have a look at their implementation:
import qualified Data.STRef as ST
newSTRef = strictToLazyST . ST.newSTRef
readSTRef = strictToLazyST . ST.readSTRef
writeSTRef r a = strictToLazyST (ST.writeSTRef r a)
And there's the culprit. Data.STRef.Lazy
is actually implemented via Data.STRef
, which is meant for Control.Monad.ST.Strict
. strictToLazyST
only hides this detail:
strictToLazyST :: ST.ST s a -> ST s a
strictToLazyST m = ST $ \s ->
Convert a strict ST computation into a lazy one. The strict state thread passed to strictToLazyST
is not performed until the result of the lazy state thread it returns is demanded.
Now lets put things together:
- in
main
, we want to print
the value given by the lazy ST
computation
- the lazy
ST
computation's value is given by a lazy readSTRef
- the lazy
readSTRef
is actually implemented as a lazy wrapper around the strict readSTRef
- the strict
readSTRef
evaluates the state as if it was a strict one
- the strict evaluation of
forever $ return ()
bites us
So the current ST.Lazy
is lazy enough. It's the Data.STRef.Lazy
that's too strict. As long as Data.STRef.Lazy
is based on strictToLazyST
, this behavior will endure.