I am rounding up my functional programming course and I’ve been loving so far haskell. For our last project we have to make a game, the only relevant detail is that it’s a real time game, meaning that we run frame per frame and pass our updated “world” on every frame. I was thinking about how to manage state in a game which, due to the naming, made me thinkg of state monads. But as far as I understands state monads store sequential state changes, and do not execute when binding these changes but instead only runs it when given an argument (the initial state). So it seems like using a state monad for this type of application is not a plausible idea.
The question I wanted to ask was if using a state monad for these type of applications is a bad idea, and if not how would u then handle the ‘state changes’ of the world with monads?
A state monad carries its state at all times. At any point, you can modify the state, in which case it immediately updates and becomes available to read.
Let me show you an example. Imagine I had all of the state update logic in a single function. Its type signature would look like this:
where FrameInput includes everything that can make the game state change, e.g. the current time, player controls, etc, and your game state is a type GameState. Your state monad has the ability to perform some IO actions (e.g. networking), and returns a Frame to draw on the screen.
It’s true that you’ll need an initial state to get going, but the monad state doesn’t store changes, it stores the actual state. Your game loop would look like:
run = let initialState = ...
in evalStateT initialState $ forever $ do
newInputs <- ...
frame <- runFrame newInputs
display frame
Strict StateT only evaluates your state to WHNF, so it’s your responsibility to make sure it’s fully evaluated before putting it back. It won’t matter much in your case due to synchronous rendering (which will automatically evaluate most of the data structure), but evaluating late is both a performance hit (in this case) and a potential memory leak landmine;
There’s a difference between a well-formed end-of-frame World and a potentially-ill-formed middle-of-frame “world”, and it’s a line that StateT blurs. You will never need the entire “world” for any given action in between, so you could avoid that notion entirely by only ever assembling the World once at the end of each frame.
I would argue StateT doesn’t help with anything here, so there’s no reason to use it, but I wouldn’t go so far as to say it’s a “bad idea”.
I see advancing the world and rendering as two independent actions:
advance
:: [Input] -- ^ all player input events emitted during the last frame
-> World -- ^ only pattern matched on once
-> IO World -- ^ only constructed once
render
:: World -- ^ includes rendering state
-> IO ()
I’d recommend adding th-deepstrict to your alternatives; I think it could work quite well with “correct by construction” strictness, but letting you identify laziness in your structures.
That’s a good idea, thanks. th-deepstrict is complementary to the libraries and techniques I mention in the article, which are for removing unintended laziness. th-deepstrict is for detecting which unintended laziness you might want to remove. Detection and mitigation work best when paired! Tracking this as: Mention th-deepstrict in make-invalid-laziness-unrepresentable · Issue #6 · tomjaguarpaw/H2 · GitHub