了解嵌套的 Monad 约束

Understanding nested Monad constraints

假设我有以下类型:

data Row = Row
  { 
    id                          :: !AddressID
  }

具有以下内部转换功能:

makeAddress :: MonadIO m => MonadError Error m => Connection -> Row -> m Address
makeAddress _ Row{..} = return $ Address "Potato"

然后我有以下函数使用Postgres.Simple从数据库中读取:

findMany
  :: MonadIO m
  => MonadReader Context m
  => MonadError Error m
  => [AddressID]
  -> m [Address]

findMany ids = do
  db <- view Context.db
  xs <- liftIO $ PG.query db sql_query_addr $ PG.Only (PG.In (map unAddressId ids))
  if (length xs) == (length ids)
    then do
      let addresses = concat (map (makeAddress db) xs)
      return addresses
    else
      throwError $ AddressNotFound Nothing

-----------------------------------------------------------------------------------------------------------------------

sql_query_addr :: PG.Query
sql_query_addr = [qms|
  SELECT *
  FROM addresses a
  WHERE a.id in ?
|]

编译失败:

    • Could not deduce (MonadIO [])
        arising from a use of ‘makeAddress’
      from the context: (MonadIO m, MonadReader Context m,
                         MonadError Error m)
        bound by the type signature for:
                   findMany :: forall (m :: * -> *).
                               (MonadIO m, MonadReader Context m, MonadError Error m) =>
                               [AddressID] -> m [Address]
        at app/Impl/ReadModelApi/FindMany.hs:(22,1)-(27,18)
    • In the first argument of ‘map’, namely ‘(makeAddress db)’
      In the first argument of ‘concat’, namely
        ‘(map (makeAddress db) xs)’
      In the expression: concat (map (makeAddress db) xs)
   |
34 |       let quotations = concat (map (makeAddress db) xs)
   |                                     ^^^^^^^^^^^^^^^^^

我意识到我的 makeAddress 函数不必要地复杂,这是一个最小的案例,从一个更大、更多副作用的转换函数中归结出来。

但是我不明白为什么编译失败,我以为:

鉴于此类型:makeAddress :: MonadIO m => MonadError Error m => Connection -> Row -> m AddressmakeAddress db 的类型是 MonadIO m => MonadError Error m -> Row -> m Address。给定 xs 的类型为 [Row]map (makeAddress db) xs 应该给出 [Addresses].

并且鉴于内部和外部 m(在 makeAddressfindMany 中)都是 MonadIO 类型类的实例,这些应该是兼容的单子?

显然这是不正确的,但我不知道我的推理在哪里失败,也不知道如何修复我的实现。

concat (map f list) 需要 f 到 return 一个列表。这样 map f list 就可以生成列表列表 concat

因此,在您的代码中,您使用 makeAddress 选择 m = [] 以便 map (makeAddress ...) xs :: [[Address]]concat (....) :: [Address]。现在,makeAddress 要求 monad m 在 class MonadIO 中,但 m = [] 不是,因此出现错误。

尝试使用类似

的东西
...
then mapM (makeAddress db) xs
else ...

你说:

makeAddress :: MonadIO m => MonadError Error m => Connection -> Row -> m Address

当然可以。并且:

makeAddress db :: MonadIO m => MonadError Error m -> Row -> m

足够接近了。最后实际上是 m Address,但我认为这只是一个错字。并且:

map (makeAddress db) xs :: [Address]

这是你的第一个错误。你输了m!它实际上是:

map (makeAddress db) xs :: MonadIO m => MonadError Error m => [m Address]

错误的解释是我们有

concat :: [[a]] -> [a]

因此,要使 [m Address] 等于 [[a]],我们必须选择 m ~ []a ~ Address¹;但是 [] 不是可以执行 IO 的 monad,因此不满足 MonadIO m 约束。哎呀!

您可以使用 sequenceA:

而不是 concat
sequenceA :: Applicative m => [m a] -> m [a]
-- OR, specializing,
sequenceA :: MonadIO m => MonadError m => [m Address] -> m [Address]

这个map-sequenceA组合很常见,它有自己的名字:

traverse :: Applicative m => (a -> m b) -> [a] -> m [b]

¹ 如果您以前没有见过 ~,您可以将此答案中的任何地方替换为 =,并且不会丢失任何重要内容。