16

我正在使用scottyand开发 Haskell 服务器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

wheregetPeople和是分别返回和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

但它可以与返回许多不同类型的数据库查询一起使用,就像我最初所做的那样。我认为我在这里对类型有一些基本的误解,但我不知道如何找出它是什么!任何帮助将不胜感激。

编辑:

在 Yuras 的建议下,我添加了这个:

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所必需的。但是,编译器错误仍然相同。

编辑:

评论者的胜利。这允许代码编译:

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

我很感激,但仍然很困惑!

代码目前看起来像thisthis

4

3 回答 3

20

似乎 GHC 是在说实际上 runDB 的类型以某种方式变得专业化。

你的猜测是对的。您原来的类型是app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM (). 这意味着您runDB的类型参数SqlPersistT IO a -> m a可以用于任何一种类型a。然而,主体app想要使用runDB两种不同类型的参数(PersonFood),所以我们需要传递一个可以用于主体中任意数量的不同类型的参数。因而app需要类型

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

(我建议将MonadIO约束放在外面,forall但你也可以把它放在里面。)

编辑:

幕后发生的事情如下:

(F a -> G a) -> X意味着forall a. (F a -> G a) -> X,这意味着/\a -> (F a -> G a) -> X/\是类型级别的 lambda。也就是说,调用者可以为特定的选择传入一个类型a和一个类型的函数。F a -> G aa

(forall a. F a -> G a) -> X意味着(/\a -> F a -> G a) -> X调用者必须传入一个函数,被调用者可以专门针对a.

于 2015-02-05T11:12:20.757 回答
7

让我们玩游戏:

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,所以我什至不会尝试。网络资源够多。

于 2015-02-05T11:05:14.457 回答
6

要真正回答显然继续使您感到困惑的标题问题:当您不提供显式签名时,Haskell 总是为函数选择最通用的 rank-1 类型。所以对于app表达式app (runDB' pool),GHC 会尝试有类型

app :: DBRunner ActionM a -> ScottyM ()

这实际上是简写

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

这是 rank-1 多态性,因为所有类型变量都是在签名之外引入的(签名本身没有量化;参数DBRunner ActionM a实际上是单态的,因为a在那一点上是固定的)。实际上,它可能是最通用的类​​型:它可以使用多态参数,例如(runDB' pool),但也可以使用单态参数。

但事实证明 can't 的实现app不能提供这种通用性:它需要一个多态动作,否则它无法a为该动作提供两种不同类型的值。因此您需要手动请求更具体的类型

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

这是 rank-2,因为它有一个包含 rank-1 多态参数的签名。GHC 无法真正知道这是您想要的类型——表达式没有明确定义的“最一般可能的 rank-n 类型”,因为您总是可以插入额外的量词。因此,您必须手动指定 rank-2 类型。

于 2015-02-05T11:53:58.207 回答