Когда общая функция не является общей?
Я работаю на сервере 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.