【问题标题】:When is a generic function not generic?什么时候泛型函数不是泛型的?
【发布时间】:2015-04-05 04:18:26
【问题描述】:

我正在使用 scottypersistent 在 Haskell 服务器上工作。许多处理程序需要访问数据库连接池,所以我开始在整个应用程序中传递连接池,以这种方式:

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

其中getPeoplegetFoods 是适当的persistent 数据库操作,它们分别返回[Person][Food]

在池上调用 liftIOrunSqlPool 的模式会在一段时间后变得令人厌烦 - 如果我可以将它们重构为一个函数,就像 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 ()

typedef 需要 -XRankNTypes。但是,编译器错误仍然相同。

编辑:

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

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

对此我很感激,但仍然感到困惑!

代码目前看起来像thisthis

【问题讨论】:

  • 尝试使用明确的forall 将类型签名添加到app,您很可能会看到问题所在。
  • @Yuras 我认为这个问题的一部分是我目前对明确的forall 的理解非常差,但我会努力这样做。
  • 我猜应该是app :: (forall a. DBRunner ActionM a) -&gt; ScottyM ()
  • @TomEllis 我应该为此启用任何特定的扩展程序吗?它仍然给我同样的错误。编辑:好的,道歉 - 我让我的处理程序在不同的功能中。在那里添加类型签名并根据需要启用更多扩展(RankNTypesFlexibleContexts)后,我开始看到新的错误。会回来报告的。
  • 你能发布你的完整代码吗?我想尝试一下,但我不想猜测你的进口是什么等等。

标签: haskell polymorphism scotty


【解决方案1】:

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

你的猜测是正确的。你原来的类型是app :: (MonadIO m) =&gt; (SqlPersistT IO a -&gt; m a) -&gt; ScottyM ()。这意味着runDB 类型的SqlPersistT IO a -&gt; m a 参数可以用于任何one 类型a。但是,app 的主体希望在两种不同的类型(PersonFood)上使用 runDB 参数,因此我们需要传递一个可用于主体中任意数量的不同类型的参数。因此app 需要类型

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

(我建议将MonadIO 约束保留在forall 之外,但您也可以将其放在里面。)

编辑:

幕后发生的事情如下:

(F a -&gt; G a) -&gt; X 表示forall a. (F a -&gt; G a) -&gt; X,表示/\a -&gt; (F a -&gt; G a) -&gt; X/\ 是类型级别的 lambda。也就是说,调用者可以为a特定选择传入一个a 类型和一个F a -&gt; G a 类型的函数。

(forall a. F a -&gt; G a) -&gt; X 表示(/\a -&gt; F a -&gt; G a) -&gt; X,调用者必须传入一个函数,被调用者可以专门针对a许多选择。

【讨论】:

  • 但这与具有通用函数可以作用于多种类型的直观方式如何交互?为什么runSqlPool 可以返回不同的类型? persistent 的肠子里有没有我看不见的 forall
  • @DanielBuckmaster:不。 runSqlPool 工作的原因与 f = head ["Hi"] ++ (show $ head [1..5]) 工作的原因相同。但是,如果您使用runSqlPool 作为参数以获得不同的a,您会遇到同样的问题——a 在第一次遇到时就被修复了。
  • @Zeta 我明白了,所以将它作为参数传递可以修复它的类型。这是有道理的,我必须传递runDB,因为它依赖于在运行时构造的池。我无法将runDB(没有')定义为库函数,只有runDB'。我猜这就是与runSqlPool 的区别。
【解决方案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 给出很好的解释,所以我什至不会尝试。网络资源充足。

【讨论】:

  • 谢谢,这是我试图在脑海中思考的那种逻辑。我会查一下RankNTypes,看看我能不能理解它。
【解决方案3】:

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

app :: DBRunner ActionM a -> ScottyM ()

实际上是简写

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

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

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

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

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

【讨论】:

  • 这与 Tom Ellis 对 forall 的解释结合起来引入了一个类型级别的 lambda,真的帮助了我。谢谢你的麻烦!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 2013-07-18
  • 1970-01-01
  • 1970-01-01
  • 2012-12-28
相关资源
最近更新 更多