Search code examples
haskellpolymorphismscotty

When is a generic function not generic?


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!

The code is currently looking like this and this.


Solution

  • It seems like GHC is saying that in fact the type of runDB becomes specialised somehow.

    Your guess is right. Your original type was app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM (). This means that your runDB argument of type SqlPersistT IO a -> m a can be used at any one type a. However, the body of app wants to use the runDB argument at two different types (Person and Food) so instead we need to pass an argument that can work for any number of different types in the body. Thus app needs the type

    app :: MonadIO m => (forall a. SqlPersistT IO a -> m a) -> ScottyM ()
    

    (I would suggest keeping the MonadIO constraint outside the forall but you can also put it inside.)

    EDIT:

    What's going on behind the scenes is the following:

    (F a -> G a) -> X means forall a. (F a -> G a) -> X, which means /\a -> (F a -> G a) -> X. /\ is the type-level lambda. That is, the caller gets to pass in a single type a and a function of type F a -> G a for that particular choice of a.

    (forall a. F a -> G a) -> X means (/\a -> F a -> G a) -> X and the caller has to pass in a function which the callee can specialise to many choices of a.