繁体   English   中英

通用函数何时不通用?

[英]When is a generic function not generic?

我正在使用scottypersistentscotty 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是分别返回[Person][Food]适当persistent数据库操作。

runSqlPool时间内调用一个池上的liftIOrunSqlPool的模式变得令人厌烦 - 如果我能将它们重构为一个函数,如Yesod的runDB ,它只会接受查询并返回适当的类型,这不是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的类型在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 ()

对此我感激不尽,但仍然神秘!

目前的代码看起来像这个这个

似乎GHC说实际上runDB的类型在某种程度上变得专业化。

你的猜测是正确的。 你的原始类型是app :: (MonadIO m) => (SqlPersistT IO a -> ma) -> ScottyM () 这意味着您的类型为SqlPersistT IO a -> ma runDB参数可以在任何一种类型中a 但是, app的主体想要在两种不同的类型( PersonFood )中使用runDB参数,所以我们需要传递一个可以适用于正文中任意数量的不同类型的参数。 因此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 forall a. (F a -> G a) -> X ,表示/\\a -> (F a -> G a) -> X /\\是类型级lambda。 即,呼叫者获得在单一类型的传递a和类型的函数F a -> G a用于该特定的选择a

(forall a. F a -> G a) -> X手段(/\\a -> F a -> G a) -> X和呼叫者具有其中被叫者可以专注于很多选择一个函数来传递a

让我们玩游戏:

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有很好的解释,所以我甚至都不会尝试。 网络上有足够的资源。

要真正回答明显继续使你神秘化的标题问题:当你不提供显式签名时,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)这样的多态参数,但也可以使用单态参数。

但事实证明,执行app不能提供该普遍性:它需要一个多态的动作,否则就不能养活两种不同类型的a值,该动作。 因此,您需要手动请求更具体的类型

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM