I'm trying to write a Monad which renders some HTML, while tracking (and caching) a few specific function calls. Here's what I tried:
data TemplateM a = TemplateM
{ templateCache :: ![(Text, Text)]
, templateResult :: !(IO a)
}
Here's how I plan to use this:
renderCached :: Text -> TemplateM Text
renderCached k =
-- lookup templateCache from the monadic context, if it lacks the key,
-- then fetch the key from an external data source (which is where the
-- "IO interior" comes from, and store it in templateCache (monadic context)
Notably, I do not want arbitrary IO actions to be executed in TemplateM via lift, liftIO, and suchlike. The only IO that should happen in TemplateM is to fetch something from the cache via the renderCached function.
I was able to define the Functor and Applicative instances for this, but got completely stuck with the Monad instance. Here's how far I got:
instance Functor TemplateM where
{-# INLINE fmap #-}
fmap fn tmpl = tmpl{templateResult=fmap fn (templateResult tmpl)}
instance Applicative TemplateM where
{-# INLINE pure #-}
pure x = TemplateM
{ templateCache = []
, templateResult = pure x
}
{-# INLINE (<*>) #-}
fn <*> f =
let fnCache = templateCache fn
fnFunction = templateResult fn
fCache = templateCache f
fResult = templateResult f
in TemplateM { templateCache = fnCache <> fCache
, templateResult = fnFunction <*> fResult
}
Is there any way to write the Monad instance for this without exposing the IO internals to the outside world?