Когда общая функция не является общей?

Я работаю на сервере Haskell, используя scotty и persistent. Многие обработчики нуждаются в доступе к пулу подключений к базе данных, поэтому я решил передать пул по всему приложению таким образом:

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

где getPeople и getFoods являются подходящими действиями базы данных persistent, которые возвращают [Person] и [Food] соответственно.

Образ вызова liftIO и runSqlPool в пуле становится утомительным через некоторое время - не было бы здорово, если бы я мог реорганизовать их в одну функцию, например Yesod runDB, которая просто взяла бы запросить и вернуть соответствующий тип. Моя попытка написать что-то вроде этого:

runDB' :: (MonadIO m) => ConnectionPool -> SqlPersistT IO a -> m a
runDB' pool q = liftIO $ runSqlPool q pool

Теперь я могу написать это:

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

За исключением того, что GHC жалуется:

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'

Кажется, что GHC говорит, что на самом деле тип runDB становится каким-то образом специализированным. Но тогда как определяются функции, такие как runSqlPool? Его подпись типа похожа на мою:

runSqlPool :: MonadBaseControl IO m => SqlPersistT m a -> Pool Connection -> m a

но он может использоваться с запросами базы данных, которые возвращают много разных типов, как я делал изначально. Я думаю, что есть что-то фундаментальное, я неправильно понимаю типы здесь, но я не знаю, как узнать, что это такое! Любая помощь будет принята с благодарностью.

EDIT:

по предложению Юраса, я добавил следующее:

type DBRunner m a = (MonadIO m) => SqlPersistT IO a -> m a
runDB' :: ConnectionPool -> DBRunner m a
app :: forall a. DBRunner ActionM a -> ScottyM ()

для которого требуется -XRankNTypes для typedef. Однако ошибка компилятора по-прежнему идентична.

EDIT:

Победа комментаторам. Это позволяет компилировать код:

app :: (forall a. DBRunner ActionM a) -> ScottyM ()

За что я благодарен, но все еще озадачен!

В настоящее время код выглядит как this и this.

Ответы

Ответ 1

Кажется, что GHC говорит, что на самом деле тип runDB каким-то образом становится специализированным.

Ваша догадка правильная. Ваш оригинальный тип был app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM (). Это означает, что ваш аргумент runDB типа SqlPersistT IO a -> m a может использоваться для любого типа a. Тем не менее, тело app хочет использовать аргумент runDB для двух разных типов (Person и Food), поэтому вместо этого нам нужно передать аргумент, который может работать для любого количества различных типов в теле. Таким образом, app нужен тип

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

(Я бы предложил сохранить ограничение MonadIO вне forall, но вы также можете поместить его внутри.)

EDIT:

Что происходит за кулисами:

(F a -> G a) -> X означает forall a. (F a -> G a) -> X, что означает /\a -> (F a -> G a) -> X. /\ - это лямбда уровня уровня. То есть, вызывающий получает передачу в одном типе a и функции типа F a -> G a для этого конкретного выбора a.

(forall a. F a -> G a) -> X означает (/\a -> F a -> G a) -> X, и вызывающий должен передать функцию, которую вызываемый может специализировать на множестве вариантов a.

Ответ 2

Позволяет играть в игру:

Prelude> let f str = (read str, read str)
Prelude> f "1" :: (Int, Float)
(1,1.0)

Работает как ожидалось.

Prelude> let f str = (read1 str, read1 str) where read1 = read
Prelude> f "1" :: (Int, Float)
(1,1.0)

Работает тоже.

Prelude> let f read1 str = (read1 str, read1 str)
Prelude> f read "1" :: (Int, Float)

<interactive>:21:1:
    Couldn't match type ‘Int’ with ‘Float’
    Expected type: (Int, Float)
      Actual type: (Int, Int)
    In the expression: f read "1" :: (Int, Float)
    In an equation for ‘it’: it = f read "1" :: (Int, Float)

Но это не так. Какая разница?

Последний f имеет следующий тип:

Prelude> :t f
f :: (t1 -> t) -> t1 -> (t, t)

Таким образом, это не работает по понятной причине, оба элемента кортежа должны иметь один и тот же тип.

Исправление выглядит так:

Prelude> :set -XRankNTypes 
Prelude> let f read1 str = (read1 str, read1 str); f :: (Read a1, Read a2) => (forall a . Read a => str -> a) -> str -> (a1, a2)
Prelude> f read "1" :: (Int, Float)
(1,1.0)

Вряд ли я могу прийти с хорошим объяснением RankNTypes, поэтому я даже не попробую. В сети достаточно ресурсов.

Ответ 3

Чтобы действительно ответить на вопрос заголовка, который, по-видимому, продолжает вас мистифицировать: Haskell всегда выбирает наиболее общий тип ранга-1 для функции, когда вы не даете явной подписи. Итак, для app в выражении app (runDB' pool) GHC попытается иметь тип

app :: DBRunner ActionM a -> ScottyM ()

который на самом деле сокращен для

app :: forall a. ( DBRunner ActionM a -> ScottyM () )

Это полиморфизм 1-го ранга, потому что все переменные типа вводятся вне сигнатуры (в самой сигнатуре не происходит квантификация, аргумент DBRunner ActionM a на самом деле мономорфен, так как a фиксируется в этой точке). На самом деле, это самый общий тип: он может работать с полиморфным аргументом, например (runDB' pool), но также будет в порядке с мономорфными аргументами.

Но оказывается, что реализация app не может предложить эту общность: она нуждается в полиморфном действии, иначе она не сможет передать два разных типа значений a этому действию. Поэтому вам нужно вручную запросить более конкретный тип

app :: (forall a. DBRunner ActionM a) -> ScottyM ()

который является рангом-2, потому что он имеет подпись, которая содержит полиморфный аргумент ранга-1. GHC не может действительно знать, что это тип, который вы хотите – там нет четко определенного "наиболее общего возможного типа ранга-n" для выражения, поскольку вы всегда можете использовать дополнительные квантификаторы. Поэтому вы должны вручную указать тип ранга 2.