Search code examples
haskellmonad-transformershaskell-persistent

How to fix missing instance of IO for a function constrained on MonadReader and MonadIO?


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.


Solution

  • 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.