如何为受 MonadReader 和 MonadIO 约束的函数修复缺少的 IO 实例?

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

我一直在努力更好地理解 mtl by building a project using it in combination with persistent

该项目的一个模块具有使用 insertMany_

的函数
service
  :: (MonadReader ApplicationConfig m, MonadIO m) =>
     ReaderT SqlBackend (ExceptT ApplicationError m) ()
service = insertMany_ =<< lift talkToAPI

此处 talkToAPI 可能会失败,因此响应被包装在 ExceptT 中,其类型为

ExceptT ApplicationError m [Example]

简而言之,service 的工作是与 API 对话,解析响应并使用 insertMany_ 将该响应存储到数据库中。

实际存储操作由withPostgresqlConn

处理
withPostgresqlConn
  :: (MonadUnliftIO m, MonadLogger m) =>
     ConnectionString -> (SqlBackend -> m a) -> m a

在我的 service 函数上使用 runReaderT 得到

ghci> :t runReaderT service
ghci> (MonadReader ApplicationConfig m, MonadIO m) =>
       SqlBackend -> ExceptT ApplicationError m ()

所以要处理这个我相信我需要像这样使用runExceptT

runService :: ConnectionString -> IO ()
runService connStr = either print return
  =<< runStdLoggingT (runExceptT $ withPostgresqlConn connStr $ runReaderT service)

但是我得到了这两个错误

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

这可能是什么问题?我这边可能有误,但我不确定去哪里找。

一个问题是 ExceptT ApplicationError IO doesn't—and in fact can't—have an MonadUnliftIO 实例。很少有 monad 有这样的实例:IO(平凡的情况)以及 IdentityReader-like transformers over IO.

解决方案是 "peel" ExceptT 构造函数 service 传递给 withPostgresqlConn 之前,而不是之后。也就是说,传递 SqlBackend -> m (Either ApplicationError ()) 值而不是 SqlBackend -> ExceptT ApplicationError m () 值。您可以通过使用 runExceptT.

组合函数来获得它

我们仍然需要 select m 的具体类型,以便它满足 service 所需的 MonadReader ApplicationConfig m, MonadIO m 约束以及 withPostgresqlConn 所需的 MonadUnliftIO m, MonadLogger m 约束。 (实际上,我们可以忘记 MonadIO,因为 MonadUnliftIO 无论如何都暗示了它)。

在您的代码中,您调用 runStdLoggingT 并期望下降到 IO。这意味着 m 应该是 LoggingT IO。这很好,因为 LoggingT 有一个 MonadUnliftIO 实例,当然还有一个 MonadLogger 实例。但是有一个问题:什么满足 MonadReader ApplicationConfig 约束?配置从哪里来?这就是第二个错误的原因。

解决方案是使 m 类似于 ReaderT ApplicationConfig (LoggingT IO)runService 函数应采用额外的 ApplicationConfig 参数,并在调用 runStdLoggingT.

之前使用配置调用 runReader

一个更普遍的观点是,monad 转换器通常有 "passthrough" 个实例,这些实例表示 "if the base monad is an instance of typeclass C, then the transformed monad is also an instance of C" 之类的东西。例如 MonadLogger m => MonadLogger (ReaderT r m)MonadUnliftIO m => MonadUnliftIO (ReaderT r m)。但是这样的实例并不总是存在于每个转换器类型类组合中。