Search code examples
haskelliomonadstransformationmonad-transformers

Understanding nested Monad constraints


Say I have the following type:

data Row = Row
  { 
    id                          :: !AddressID
  }

with the following internal transformation function:

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

I then have the following function to read from a database using 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 ?
|]

This fails to compile with:

    • 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)
   |                                     ^^^^^^^^^^^^^^^^^

I realize that my makeAddress function is needlessly complex, this is a minimal case, boiled down from a much larger, more sideffecty transformation function.

But I don't understand why this fails to compile, I would have thought that:

Given this type: makeAddress :: MonadIO m => MonadError Error m => Connection -> Row -> m Address, the type of makeAddress db is MonadIO m => MonadError Error m -> Row -> m Address. Given xs has type [Row], map (makeAddress db) xs should give [Addresses].

And given that both the inner and outer m (in makeAddress and in findMany) is an instance of the MonadIO typeclass, these should be compatible monads?

Clearly this is incorrect, but I have no idea where my reasoning breaks down, or how to therefore fix my implementation.


Solution

  • You say:

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

    Sure. And:

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

    Close enough. It's actually m Address at the end, but I assume this was just a typo. And:

    map (makeAddress db) xs :: [Address]
    

    This is your first mistake. You have lost the m! It is actually:

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

    The explanation for the error is that we have

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

    and so, for [m Address] to be equal to [[a]], we must choose m ~ [] and a ~ Address¹; but then [] is not a monad that can do IO, so the MonadIO m constraint isn't satisfied. Whoops!

    Instead of concat, you can use sequenceA:

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

    This map-sequenceA combo is so common, it has its own name:

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

    ¹ If you haven't seen ~ before, you may replace it with = everywhere in this answer and nothing of importance will be lost.