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 whereimport Control.Monad.Stateimport Control.Monad.IO.Class-- Module----------------------------------------------------------------------------------------newtype Module m a b = Module (a -> m (b, Module m a b)){-instance (Monad m) => Applicative (Module m a)instance (Monad m) => Arrow (Module m)instance (Monad m) => Category (Module m)instance (Monad m) => Functor (Module m a)-}-- GraphicsModule----------------------------------------------------------------------------------------data GraphicsState = GraphicsState Intrender :: (MonadState GraphicsState m, MonadIO m) => Int -> m ()render x = do (GraphicsState s) <- get liftIO $ print $ x + s put . GraphicsState $ s + 1type GraphicsModule = Module IO Int ()initialGraphicsState = GraphicsState 0createGraphicsModule :: GraphicsState -> GraphicsModule createGraphicsModule initialState = Module $ \x -> do (r, s') <- runStateT (render x) initialState return (r, createGraphicsModule s') initialGraphicsModule = createGraphicsModule initialGraphicsStaterunModule (Module m) x = m x-- Program----------------------------------------------------------------------------------------data ProgramState = ProgramState { graphicsModule :: GraphicsModule}renderInProgram :: (MonadState ProgramState m, MonadIO m) => Int -> m ()renderInProgram x = do gm <- gets graphicsModule (r, gm') <- liftIO $ runModule gm x modify $ \g -> g { graphicsModule = gm' }initialProgramState = ProgramState initialGraphicsModulemain = runStateT prog initialProgramStateprog = 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.