Polymorphic components for graphics and program state I asked this question on StackOverflow, got some answers, most notably a link to this one, and basing on that I've implemented this:

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}

module Main where

import Control.Monad.State
import Control.Monad.IO.Class

-- Module
----------------------------------------------------------------------------------------
newtype Module m a b =
    Module (a -&gt; m (b, Module m a b))

{-
instance (Monad m) =&gt; Applicative (Module m a)
instance (Monad m) =&gt; Arrow (Module m)
instance (Monad m) =&gt; Category (Module m)
instance (Monad m) =&gt; Functor (Module m a)
-}

-- GraphicsModule
----------------------------------------------------------------------------------------
data GraphicsState = GraphicsState Int

render :: (MonadState GraphicsState m, MonadIO m) =&gt; Int -&gt; m ()
render x = do
    (GraphicsState s) &lt;- get
    liftIO $ print $ x + s
    put . GraphicsState $ s + 1

type GraphicsModule = Module IO Int ()

initialGraphicsState = GraphicsState 0

createGraphicsModule :: GraphicsState -&gt; GraphicsModule 
createGraphicsModule initialState = Module $ \x -&gt; do
    (r, s') &lt;- runStateT (render x) initialState
    return (r, createGraphicsModule s') 

initialGraphicsModule = createGraphicsModule initialGraphicsState

runModule (Module m) x = m x

-- Program
----------------------------------------------------------------------------------------
data ProgramState = ProgramState {
    graphicsModule :: GraphicsModule
}

renderInProgram :: (MonadState ProgramState m, MonadIO m) =&gt; Int -&gt; m ()
renderInProgram x = do
    gm &lt;- gets graphicsModule
    (r, gm') &lt;- liftIO $ runModule gm x

    modify $ \g -&gt; g { graphicsModule = gm' }

initialProgramState = ProgramState initialGraphicsModule

main = runStateT prog initialProgramState

prog = do
    renderInProgram 1
    renderInProgram 1
    renderInProgram 1


I can see how this could be quite easily extended to allow more functions in a module (instead of just render). I am not sure if I'm keeping the state correctly, though. That was the only way I saw to not expose the inner, stateful context (note that the outer monad to the module is just IO).

Also I am aware of the fact that Lens could make it less verbose. I deliberately chose to not depend on Lens, and I think it's really functionally equivalent.
The point of the Module approach is that you don't manage any state globally. Instead, each component (Module) manages its own state internally and you just express how they are connected together.

To give a simple example, let me first implement the standard type classes for Module:

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}

module Main where

import Prelude hiding ((.), id)
import Control.Applicative
import Control.Arrow
import Control.Category
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans
import Control.Monad.IO.Class
import Data.Void

-- Module
----------------------------------------------------------------------------------------
newtype Module m a b =
    Module { runModule :: a -&gt; m (b, Module m a b) }

instance (Monad m) =&gt; Category (Module m) where
    id = Module $ \x -&gt; return (x, id)
    (Module g) . (Module f) = Module $ \x -&gt; do
        (u, f') &lt;- f x
        (v, g') &lt;- g u
        return (v, g' . f')

mkGen :: (Monad m) =&gt; (a -&gt; m b) -&gt; Module m a b
mkGen f = let m = Module $ \x -&gt; do
                    y &lt;- f x
                    return (y, m)
           in m

instance (Monad m) =&gt; Arrow (Module m) where
    arr f = mkGen (return . f)
    first (Module f) = Module $ \(x, y) -&gt; do
        (r, m) &lt;- f x
        return ((r, y), first m)

instance (Monad m) =&gt; Functor (Module m a) where
    fmap f (Module k) = Module $ \x -&gt; do
        (y, k') &lt;- k x
        return (f y, fmap f k')

instance (Monad m) =&gt; Applicative (Module m a) where
    pure x = let m = Module $ \_ -&gt; return (x, m)
              in m
    (Module f) &lt;*&gt; (Module k) = Module $ \x -&gt; do
        (h, f') &lt;- f x
        (y, k') &lt;- k x
        return (h y, f' &lt;*&gt; k')


The above functions allow creating modules and combining them together in various ways.

Now one of the main functions is to step a module with no input/output, producing its next state:

step :: (Monad m) =&gt; Module m () () -&gt; m (Module m () ())
step (Module k) = liftM snd (k ())


For example, in your case you'd have a counter module, that keeps an internal state:

counter :: (Monad m) =&gt; Module m () Int
counter = let m i = Module $ \_ -&gt; return (i, m (i + 1))
           in m 0


And a state-less module that just prints what it gets and has no output

render :: (MonadIO m, Show a) =&gt; Module m a ()
render = mkGen (liftIO . print)


Their combination is a module with no input or output, and stepping them prints the counter each time:

main :: IO ()
main = do
    let m = counter &gt;&gt;&gt; render
    step m &gt;&gt;= step &gt;&gt;= step
    return ()