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 IOdoesn't—and in fact can't—have anMonadUnliftIOinstance. Few monads have that instance:IO(the trivial case) and alsoIdentityandReader-like transformers overIO.The solution is to "peel" the
ExceptTconstructor before passingservicetowithPostgresqlConn, not after. That is, passing aSqlBackend -> m (Either ApplicationError ())value instead of aSqlBackend -> ExceptT ApplicationError m ()value. You might get that by composing the function withrunExceptT.We still have to select a concrete type for
mso that it satisfies theMonadReader ApplicationConfig m, MonadIO mconstraints required byserviceand also theMonadUnliftIO m, MonadLogger mconstraints required bywithPostgresqlConn. (Actually, we can forget aboutMonadIObecauseMonadUnliftIOimplies it anyway).In your code, you invoke
runStdLoggingTand expect to get down toIO. That means thatmis expected to beLoggingT IO. That's fine becauseLoggingThas aMonadUnliftIOinstance, and of course aMonadLoggerone. There's a problem though: what satisfies theMonadReader ApplicationConfigconstraint? Where's the config coming from? That's the cause of the second error.The solution is to make
msomething likeReaderT ApplicationConfig (LoggingT IO). TherunServicefunction should take an additionalApplicationConfigparameter, and invokerunReaderwith the config before invokingrunStdLoggingT.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)ofMonadUnliftIO m => MonadUnliftIO (ReaderT r m). But such instances don't always exist for every transformer-typeclass combination.