I'm working on a Haskell server using scotty
and persistent
. Many handlers need access to the database connection pool, so I've taken to passing the pool around throughout the app, in this sort of fashion:
main = do
runNoLoggingT $ withSqlitePool ":memory:" 10 $ \pool ->
liftIO $ scotty 7000 (app pool)
app pool = do
get "/people" $ do
people <- liftIO $ runSqlPool getPeople pool
renderPeople people
get "/foods" $ do
food <- liftIO $ runSqlPool getFoods pool
renderFoods food
where getPeople
and getFoods
are appropriate persistent
database actions that return [Person]
and [Food]
respectively.
The pattern of calling liftIO
and runSqlPool
on a pool becomes tiresome after a while - wouldn't it be great if I could refactor them into a single function, like Yesod's runDB
, which would just take the query and return the appropriate type. My attempt at writing something like this is:
runDB' :: (MonadIO m) => ConnectionPool -> SqlPersistT IO a -> m a
runDB' pool q = liftIO $ runSqlPool q pool
Now, I can write this:
main = do
runNoLoggingT $ withSqlitePool ":memory:" 10 $ \pool ->
liftIO $ scotty 7000 $ app (runDB' pool)
app runDB = do
get "/people" $ do
people <- runDB getPeople
renderPeople people
get "/foods" $ do
food <- runDB getFoods
renderFoods food
Except that GHC complains:
Couldn't match type `Food' with `Person'
Expected type: persistent-2.1.1.4:Database.Persist.Sql.Types.SqlPersistT
IO
[persistent-2.1.1.4:Database.Persist.Class.PersistEntity.Entity
Person]
Actual type: persistent-2.1.1.4:Database.Persist.Sql.Types.SqlPersistT
IO
[persistent-2.1.1.4:Database.Persist.Class.PersistEntity.Entity
Food]
In the first argument of `runDB', namely `getFoods'
It seems like GHC is saying that in fact the type of runDB
becomes specialised somehow. But then how are functions like runSqlPool
defined? Its type signature looks similar to mine:
runSqlPool :: MonadBaseControl IO m => SqlPersistT m a -> Pool Connection -> m a
but it can be used with database queries that return many different types, as I was doing originally. I think there's something fundamental I'm misunderstanding about types here, but I have no idea how to find out what it is! Any help would be greatly appreciated.
EDIT:
at Yuras' suggestion, I've added this:
type DBRunner m a = (MonadIO m) => SqlPersistT IO a -> m a
runDB' :: ConnectionPool -> DBRunner m a
app :: forall a. DBRunner ActionM a -> ScottyM ()
which required -XRankNTypes
for the typedef. However, the compiler error is still identical.
EDIT:
Victory to the commentors. This allows the code to compile:
app :: (forall a. DBRunner ActionM a) -> ScottyM ()
For which I'm grateful, but still mystified!
app
with explicitforall
and most likely you'd see what is wrong. – Valareeforall
, but I shall endeavor to do so. – Neotenyapp :: (forall a. DBRunner ActionM a) -> ScottyM ()
. – TheatheaceousRankNTypes
andFlexibleContexts
) as required I'm starting to see new errors. Will report back. – NeotenyrunDB
stuff hasn't been pushed because obviously it isn't working :P. I can try to make a minimal example if you'd like. – Neotenytype
and just usingapp :: MonadIO m => (forall a. SqlPersistT IO a -> m a) -> ScottyM ()
. I don't think the constraint has to be inside theforall
. – Theatheaceousforall
in the typedef too... – NeotenyrunDB
call inapp
set the type variablea
inDBRunner m a
toEntity People
. Therefore, the type checker assumesrunDB :: DBRunner ActionM (Entity People)
. However, your second use ofrunDB
impliesrunDB :: DBRunner ActionM (Entity Food)
! Oh noez! You have to tell GHC thatrunDB
should take any a. And that's done with(forall a . DBRunner ActionM a) ->
. Note the parentheses, theforall
may not float out. – Broutforall
in connection withST
. Seems it didn't sink in - but I'm still confused whyrunDB
needsforall
butrunSqlPool
doesn't. – NeotenyrunSqlPool
comes from the top level. If you try to pass it in as an argument you will experience the same problem. Alternatively if you writerunDB = runDB' pool
andapp
both inside awhere
clause in the body of themain
then you will not needRank2Types
. – Theatheaceous