UpdateGame function for Tetris
You should separate IO actions from pure code. In your case the principal task of updateGame is to create the new game state given input data, and for this no IO is necessary. The fact that the state is stored and modified in an IORef.

It's not just the matter of style and code separation. Calling modifyIORef multiple times in the function means that you can easily get race conditions, if there is another thread working with the game state. It would get or modify an inconsistent state.

So my suggestion would be to split it into two parts - one a pure function, and another that does all the necessary IO:

updateGame :: UTCTime -&gt; InputsData -&gt; GameState -&gt; GameState
updateGame time input game = ...


updateGameIO :: IORef InputsData -&gt; IORef GameState -&gt; IO ()
updateGameIO modifyIORef gameIORef = step
  where
    step = do
      time &lt;- getCurrentTime

      keys &lt;- readIORef keysIORef
      modifyIORef gameIORef (updateGame time keys)

      addTimerCallback 1 step
      postRedisplay Nothing


This way, you ensure that the game state is updated atomically, and keep the main function pure.



Updating a field in a complex data type is a common task, and it can be greatly simplified using the lens library. Instead of

\g -&gt; g {keyCooldowns = updateCooldowns (keyCooldowns game) elapsed}


you let lens generate so-called lenses for your data type, so for example keyCooldowns won't be of type GameState -&gt; Something, but Lens' GameState Something, and then the function that modifies the field inside GameState would be

over keyCooldowns (flip updateCooldowns elapsed)


See A Little Lens Starter Tutorial