Reputation: 6793
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?
Upvotes: 3
Views: 112
Reputation: 48631
As others have suggested, the elementary solution here is to use StateT
. Since you don't need to store your IORef
in a data structure or share it between threads, you can eliminate it altogether. (Of course, if that changes and you do end up wanting to share state across multiple concurrent threads, you'll have to revisit this choice.)
import Control.Monad.State.Strict
import Data.Text (Text)
import Data.Tuple (swap)
newtype TemplateM a = TemplateM {unTemplateM :: StateT [(Text, Text)] IO a}
deriving (Functor, Applicative, Monad)
renderCached :: Text -> TemplateM Text
renderCached k = TemplateM $ do
v <- pure $ "rendered template for " <> k
modify ((k, v) :)
pure v
runTemplateM :: [(Text, Text)]
-> TemplateM a
-> IO ([(Text, Text)], a)
runTemplateM initialCache x = fmap swap $ flip runStateT initialCache (unTemplateM x)
It goes without saying that a cache like this should almost certainly be stored as a structure that is not a list. One promising option is to use text-trie, a data structure Wren Romano designed specially for this purpose. You might also consider a HashMap
or even a Map
.
Upvotes: 2
Reputation: 6793
I've worked out a solution sitting on top of ReaderT
, but I really want to get my original idea to work:
newtype TemplateM a = TemplateM { unTemplateM :: ReaderT (IORef [(Text, Text)]) IO a } deriving (Functor, Applicative, Monad)
renderCached :: Text -> TemplateM Text
renderCached k = TemplateM $ do
-- this is just dummy code. The actual cache lookup has not
-- been implemented, but the types align
v <- pure $ "rendered template for " <> k
cacheRef <- ask
atomicModifyIORef' cacheRef (\x -> ((k, v):x, ()))
pure v
runTemplateM :: [(Text, Text)]
-> TemplateM a
-> IO ([(Text, Text)], a)
runTemplateM initialCache x = do
initialCacheRef <- newIORef initialCache
(flip runReaderT) initialCacheRef $ do
res <- unTemplateM x
ref <- ask
finalCache <- readIORef ref
pure (finalCache, res)
Upvotes: 2