I'm using a state transformer to randomly sample a dataset at every point of a 2D recursive walk, which outputs a list of 2D grids of samples that together succeed a condition. I'd like to pull from the results lazily, but my approach instead exhausts the whole dataset at every point before I can pull the first result.
To be concrete, consider this program:
import Control.Monad ( sequence, liftM2 )
import Data.Functor.Identity
import Control.Monad.State.Lazy ( StateT(..), State(..), runState )
walk :: Int -> Int -> [State Int [Int]]
walk _ 0 = [return [0]]
walk 0 _ = [return [0]]
walk x y =
let st :: [State Int Int]
st = [StateT (\s -> Identity (s, s + 1)), undefined]
unst :: [State Int Int] -- degenerate state tf
unst = [return 1, undefined]
in map (\m_z -> do
z <- m_z
fmap concat $ sequence [
liftM2 (zipWith (\x y -> x + y + z)) a b -- for 1D: map (+z) <$> a
| a <- walk x (y - 1) -- depth
, b <- walk (x - 1) y -- breadth -- comment out for 1D
]
) st -- vs. unst
main :: IO ()
main = do
std <- getStdGen
putStrLn $ show $ head $ fst $ (`runState` 0) $ head $ walk 2 2
The program walks the rectangular grid from (x, y) to (0, 0) and sums all the results, including the value of one of the lists of State monads: either the non-trivial transformers st that read and advance their state, or the trivial transformers unst. Of interest is whether the algorithm explores past the heads of st and unst.
In the code as presented, it throws undefined. I chalked this up to a misdesign of my order of chaining the transformations, and in particular, a problem with the state handling, as using unst instead (i.e. decoupling the result from state transitions) does produce a result. However, I then found that a 1D recursion also preserves laziness even with the state transformer (remove the breadth step b <- walk... and swap the liftM2 block for fmap).
If we trace (show (x, y)), we also see that it does walk the whole grid before triggering:
$ cabal run
Build profile: -w ghc-8.6.5 -O1
...
(2,2)
(2,1)
(1,2)
(1,1)
(1,1)
sandbox: Prelude.undefined
I suspect that my use of sequence is at fault here, but as the choice of monad and the dimensionality of the walk affect its success, I can't say broadly that sequenceing the transformations is the source of strictness by itself.
What's causing the difference in strictness between 1D and 2D recursion here, and how can I achieve the laziness I want?

Consider the following simplified example:
Here, in both the 1D and 2D calculations, the head of the result depends explicitly only on the heads of the inputs (just
head afor the 1D action and bothhead aandhead bfor the 2D action). However, in the 2D calculation, there's an implicit dependency ofb(even just its head) on the current state, and that state depends on the evaluation of the entirety ofa, not just its head.You have a similar dependency in your example, though it's obscured by the use of lists of state actions.
Let's say we wanted to run the action
walk22_head = head $ walk 2 2manually and inspect the first integer in the resulting list:Writing the elements of the state action list
stexplicitly:we can write
walk22_headas:Note that this depends only on the defined state action
st1and the heads ofwalk 2 1andwalk 1 2. Those heads, in turn, can be written:Again, these depend only on the defined state action
st1and the head ofwalk 1 1.Now, let's try to write down a definition of
walk11_head:This depends only on the defined state action
st1, so with these definitions in place, if we runmain, we get a defined answer:But these definitions aren't accurate! In each of
walk 1 2andwalk 2 1, the head action is a sequence of actions, starting with the action that invokeswalk11_head, but continuing with actions based onwalk11_tail. So, more accurate definitions would be:with:
With these definitions in place, there's no problem running
walk12_headandwalk21_headin isolation:The state side effects here are not needed to calculate the answer and so never invoked. But, it's not possible to run them both in sequence:
Therefore, trying to run
mainfails for the same reason:because, in calculating
walk22_head, even the very beginning ofwalk21_head's calculation depends on the state side effectwalk11_tailinitiated bywalk12_head.Your original
walkdefinition behaves the same way as these mockups:It's hard to say how to fix this. Your toy example was excellent for the purposes of illustrating the problem, but it's not clear how the state is used in your "real" problem and if
head $ walk 2 1really has a state dependency on thesequenceofwalk 1 1actions induced byhead $ walk 1 2.