I am developing a graphical (game-like) program (using SDL for graphics) in Haskell. As part of this, there is necessarily a 'main' infinite loop, which handles the state updates and drawing, keeping track of time in the process. I previously had it working where it would take the state as a parameter like the following simplified example:
loop :: State -> Int -> IO ()
loop state prevTime = do
time <- getTicks
let state' = updateState state (time - prevTime)
loop state' time
In this, the updateState
function itself used a monad transformer stack including a StateT for state, ReaderT for config, a WriterT for logging, and a RandT for random number generation, so I had to pass all of the parameters required through to updateState
, and take the random number generator back. And there were multiple different updateState
-related functions, all of which needed some IO
done in between them, and so a lot of the code in my main loop was just threading state and parameters through.
Of course, I quickly realised that that kind of code was horrible and decided I could probably do away with it by making the whole loop run in a different transformer stack, this one with IO
at the bottom. I can then run code in my pure monad and have it automatically thread its state through with the following function:
promoteContext :: PureContext a -> IOContext a
promoteContext ctx = do
state <- get
config <- ask
gen <- liftIO getStdGen
let (result, state', log, gen') = runContext ctx gen rules state
liftIO $ setStdGen gen'
put state'
tell log
return result
This cleaned up my code massively, and it seemed like a much better solution at first, but I soon encountered a massive issue: running my program for around a minute or so suddenly started causing a stack space overflow.
I managed to somewhat work around this issue while keeping my code cleaner than it was before, by making the main loop method run in the IO
monad directly and thread the state through, meaning I only have to deal with that boilerplate once per loop. This clearly forces the state to be fully evaluated every time it loops, meaning the stack doesn't fill up, but it still feels 'dirty'.
My question is this: is there any way I can force a full evaluation of the state every time I loop, without resorting to falling back to IO
and explicitly leaving/re-entering the transformer stack?
EDIT: Thanks to suggestions by Petr Pudlák, I spent some time trying to isolate the issue, and eventually arrived at this code which causes the problem:
import Control.Monad.Writer.Strict
import Control.Monad.State.Strict
import Control.DeepSeq
import Exception
type ContextT s m = StateT s (WriterT [String] m)
evalContext ctx state = do
(a, log) <- runWriterT (evalStateT ctx state)
liftIO $ evaluate (rnf log)
return a
problem :: ContextT Double IO ()
problem = do modify (+ 0.001)
s <- get
liftIO $ print s
problem
main :: IO ()
main = evalContext problem 0
{A small edit to the code: I added the forced evaluation of the log and the problem still happens.}
Running this causes the stack space to overflow by the time the state reaches about 500. Getting rid of the WriterT
, however, stops the overflow from happening, suggesting that it was the Writer
's fault all along. I don't understand how this can be, though, as in this isolated code the writer isn't even used. I guess my question is now why on Earth does the presence of a WriterT
cause this to happen?
put $! state
ordeepseq state $ put state
wheredeepseq
is a suitable function which forces what's needed. – chiput $! state'
inpromoteContext
(which is where I assume you meant I should do that), and everything in my state record has strictness annotations all the way down toDouble
,Int
, etc, and it still overflows just as quickly. – Bradley Hardy