Embracing ReaderT finally
Intended audience: programmers, esp those with functional-programming background. My last outing with the ReaderT monad was a minor disaster and I ended up writing a custom wrapper that got the job done. At the time, the monad-stacking behavior was new to me and I couldn't make sense of the code I was getting into. Recently, I added a global-config in my blog-generator project (vāk). A handful of things needed to be dynamic and set from the shell env, and these get set into the global config which then gets passed around the app in multiple places. I thought it was time for another outing with the ReaderT monad. vāk's codebase is simple, in the sense that almost all effectful functions are just Aff a, which get rolled into top-level ExceptT functions and the main function "runs" these top-level functions. At a high-level, this just meant that I had to retain the same behavior but only change the top-level function to be ReaderT wrapping around an ExceptT and the main function just runs runReaderT and runExceptT (which I can compose into a runAppM). But of course, a lot of Aff a functions were internally calling a getConfig function (which is the global config getter). So, the idea was make those functions which called getConfig use the ReaderT monad transformer's ask function and that means those functions became ReaderT instead of Aff. And thus began the conversion of all such functions. Some functions still remained Aff because either global config was passed to them as a parameter, or they did not use the global config at all. Composing an Aff and a ReaderT meant that I had to "lift" the Aff into ReaderT. The liftAff instance does not work though because it does not "handle" errors. So, I ended up rolling my own: liftAppM :: forall a. Aff a -> AppM a liftAppM aff = ReaderT \_ -> ExceptT $ try $ aff Now, the Aff is run, errors are "caught" via try, converted into an Either and then all of that is rolled into an ExceptT, wrapped around a ReaderT leading to my AppM: type AppM a = ReaderT Config (ExceptT Error Aff) a I liked how I was able to, with this liftAppM, gradually move functions around until everything was neatly converted. For instance, if this was the pre-ReaderT function: buildSite :: Config -> ExceptT Error Aff Unit buildSite config = ExceptT $ try $ do initializeStuff config posts

Intended audience: programmers, esp those with functional-programming background.
My last outing with the ReaderT
monad was a minor disaster and I ended up writing a custom wrapper that got the job done. At the time, the monad-stacking behavior was new to me and I couldn't make sense of the code I was getting into.
Recently, I added a global-config in my blog-generator project (vāk
). A handful of things needed to be dynamic and set from the shell env, and these get set into the global config which then gets passed around the app in multiple places.
I thought it was time for another outing with the ReaderT
monad.
vāk
's codebase is simple, in the sense that almost all effectful functions are just Aff a
, which get rolled into top-level ExceptT
functions and the main
function "runs" these top-level functions. At a high-level, this just meant that I had to retain the same behavior but only change the top-level function to be ReaderT
wrapping around an ExceptT
and the main function just runs runReaderT
and runExceptT
(which I can compose into a runAppM
).
But of course, a lot of Aff a
functions were internally calling a getConfig
function (which is the global config getter). So, the idea was make those functions which called getConfig
use the ReaderT
monad transformer's ask
function and that means those functions became ReaderT
instead of Aff
.
And thus began the conversion of all such functions.
Some functions still remained Aff
because either global config was passed to them as a parameter, or they did not use the global config at all.
Composing an Aff
and a ReaderT
meant that I had to "lift" the Aff
into ReaderT
. The liftAff
instance does not work though because it does not "handle" errors. So, I ended up rolling my own:
liftAppM :: forall a. Aff a -> AppM a
liftAppM aff = ReaderT \_ -> ExceptT $ try $ aff
Now, the Aff
is run, errors are "caught" via try
, converted into an Either
and then all of that is rolled into an ExceptT
, wrapped around a ReaderT
leading to my AppM
:
type AppM a = ReaderT Config (ExceptT Error Aff) a
I liked how I was able to, with this liftAppM
, gradually move functions around until everything was neatly converted.
For instance, if this was the pre-ReaderT function:
buildSite :: Config -> ExceptT Error Aff Unit
buildSite config = ExceptT $ try $ do
initializeStuff config
posts <- processBlogPosts
-- and about 15 more lines of function calls, each in the 'Aff' monad
I could do this:
buildSite :: AppM Unit
buildSite = do
config <- ask
liftAppM $ do
initializeStuff config
posts <- processBlogPosts
-- rest
Then, I'll see that processBlogPosts
calls for config
internally, so I'd have to make it use ask
and therefore, turn it into an AppM
. This means I'd also move function above it (initializeStuff
) into an AppM
:
buildSite :: AppM Unit
buildSite = do
config <- ask
initializeStuff
posts <- processBlogPosts
liftAppM $ do
-- rest of the stuff
This way, I could gradually move things out of the inner liftAppM
and finally get rid of it altogether. Just the logging bits remained in a generic m
monad and could be lifted into my AppM
via liftAppM
so:
buildSite :: AppM Unit
buildSite = do
config <- ask
liftAppM $ Logs.logInfo "Starting..."
liftAppM $ createFolderIfNotPresent tmpFolder
postsMetadata <- generatePostsHTML
liftAppM $ Logs.logSuccess $ "Posts page generated."
liftAppM $ Logs.logInfo $ "Generating archive page..."
_ <- writeArchiveByYearPage postsMetadata
liftAppM $ Logs.logSuccess $ "Archive page generated."
liftAppM $ Logs.logInfo $ "Generating home page..."
_ <- createHomePage postsMetadata
liftAppM $ Logs.logSuccess $ "Home page generated."
liftAppM $ Logs.logInfo $ "Copying 404.html..."
_ <- liftEffect $ execSync ("cp " <> config.templateFolder <> "/404.html " <> tmpFolder) defaultExecSyncOptions
liftAppM $ Logs.logSuccess $ "404.html copied."
liftAppM $ Logs.logInfo "Copying images folder..."
_ <- liftEffect $ execSync ("cp -r " <> "./images " <> tmpFolder) defaultExecSyncOptions
liftAppM $ Logs.logSuccess $ "images folder copied."
liftAppM $ Logs.logInfo "Copying js folder..."
_ <- liftAppM $ liftEffect $ execSync ("cp -r " <> "./js " <> tmpFolder) defaultExecSyncOptions
liftAppM $ Logs.logSuccess "js folder copied."
liftAppM $ Logs.logInfo "Generating styles.css..."
_ <- generateStyles
liftAppM $ Logs.logSuccess "styles.css generated."
liftAppM $ Logs.logInfo "Generating RSS feed..."
_ <- Rss.generateRSSFeed (take 10 postsMetadata)
liftAppM $ Logs.logSuccess "RSS feed generated."
liftAppM $ Logs.logInfo $ "Copying " <> tmpFolder <> " to " <> config.outputFolder
_ <- liftAppM $ createFolderIfNotPresent config.outputFolder
_ <- liftAppM $ liftEffect $ execSync ("cp -r " <> tmpFolder <> "/* " <> config.outputFolder) defaultExecSyncOptions
liftAppM $ Logs.logSuccess "Copied."
liftAppM $ Logs.logInfo "Updating cache..."
_ <- Cache.writeCacheData
liftAppM $ Logs.logSuccess "Cached updated."
There are (monad-reader-transformer) lift
instances for both Aff
and ExceptT
but both will fail to handle errors.
Here's the implementation for ExceptT
that will get called:
instance monadTransExceptT :: MonadTrans (ExceptT e) where
lift m = ExceptT do
a <- m
pure $ Right a
If the m
action throws an error, the app just crashes. Typically, I'd like for it to be returned as a Left
value (the way non-errors are getting returned as Right
).
I am still not very clear about the mechanics here though; tests with native monad-reader-transformer instances of lift
for both Aff
and ExceptT
seem to crash the app instead of allowing the user to handle them in a custom way:
test :: Effect Unit
test = launchAff_ $ do
config <- getConfig
res <- runAppM config $ do
lift $ ExceptT $ do
testAff
liftEffect $ logShow res
testAff :: Aff (Either Error Unit)
testAff = do
throwError $ error "test error"
Running test
will crash the app.
Hence, the custom (and poorly-named) liftAppM
.