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