I have been trying to get a good understanding of mtl by building a project using it in combination with persistent.
One module of that project has a function which uses insertMany_
service
:: (MonadReader ApplicationConfig m, MonadIO m) =>
ReaderT SqlBackend (ExceptT ApplicationError m) ()
service = insertMany_ =<< lift talkToAPI
Here talkToAPI
can fail so the response is wrapped in ExceptT
, its type is
ExceptT ApplicationError m [Example]
In short the service
's job is to talk to an API, parse the response and use insertMany_
to store that response into a database.
The actual storage action is handled by withPostgresqlConn
withPostgresqlConn
:: (MonadUnliftIO m, MonadLogger m) =>
ConnectionString -> (SqlBackend -> m a) -> m a
Using runReaderT
on my service
function yields
ghci> :t runReaderT service
ghci> (MonadReader ApplicationConfig m, MonadIO m) =>
SqlBackend -> ExceptT ApplicationError m ()
so to process this I believe I need to use runExceptT
like this
runService :: ConnectionString -> IO ()
runService connStr = either print return
=<< runStdLoggingT (runExceptT $ withPostgresqlConn connStr $ runReaderT service)
But I get these two errors
• No instance for (MonadUnliftIO (ExceptT ApplicationError IO))
arising from a use of ‘withPostgresqlConn’
• No instance for (MonadReader ApplicationConfig IO)
arising from a use of ‘service’
What could be the problem here? There has likely been an error on my part but I'm not sure where to look for.
One problem is that ExceptT ApplicationError IO
doesn't—and in fact can't—have an MonadUnliftIO
instance. Few monads have that instance: IO
(the trivial case) and also Identity
and Reader
-like transformers over IO
.
The solution is to "peel" the ExceptT
constructor before passing service
to withPostgresqlConn
, not after. That is, passing a SqlBackend -> m (Either ApplicationError ())
value instead of a SqlBackend -> ExceptT ApplicationError m ()
value. You might get that by composing the function with runExceptT
.
We still have to select a concrete type for m
so that it satisfies
the MonadReader ApplicationConfig m, MonadIO m
constraints required by service
and also the MonadUnliftIO m, MonadLogger m
constraints required by withPostgresqlConn
. (Actually, we can forget about MonadIO
because MonadUnliftIO
implies it anyway).
In your code, you invoke runStdLoggingT
and expect to get down to IO
. That means that m
is expected to be LoggingT IO
. That's fine because LoggingT
has a MonadUnliftIO
instance, and of course a MonadLogger
one. There's a problem though: what satisfies the MonadReader ApplicationConfig
constraint? Where's the config coming from? That's the cause of the second error.
The solution is to make m
something like ReaderT ApplicationConfig (LoggingT IO)
. The runService
function should take an additional ApplicationConfig
parameter, and invoke runReader
with the config before invoking runStdLoggingT
.
A more general point is that monad transformers often have "passthrough" instances which say things like "if the base monad is an instance of typeclass C, then the transformed monad is also an instance of C". For example MonadLogger m => MonadLogger (ReaderT r m)
of MonadUnliftIO m => MonadUnliftIO (ReaderT r m)
. But such instances don't always exist for every transformer-typeclass combination.