泛型函数什么时候不泛型?
When is a generic function not generic?
我正在使用 scotty
和 persistent
在 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
其中 getPeople
和 getFoods
是适当的 persistent
数据库操作,分别是 return [Person]
和 [Food]
。
一段时间后,在池中调用 liftIO
和 runSqlPool
的模式变得令人厌烦 - 如果我可以将它们重构为单个函数,那不是很好吗,就像 Yesod 的 runDB
,这只会采用查询和 return 适当的类型。我尝试写这样的东西是:
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
但它可以与 return 许多不同类型的数据库查询一起使用,就像我最初所做的那样。我认为我对这里的类型有一些基本的误解,但我不知道如何找出它是什么!任何帮助将不胜感激。
编辑:
根据 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 ()
对此我很感激,但仍然感到困惑!
来玩游戏吧:
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
给出很好的解释,所以我什至不会尝试。网络资源充足
It seems like GHC is saying that in fact the type of runDB becomes specialised somehow.
你猜对了。您的原始类型是 app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM ()
。这意味着您的 runDB
类型 SqlPersistT IO a -> m a
参数可以用于任何 one 类型 a
。但是,app
的主体想要在两种不同的类型(Person
和 Food
)中使用 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
,表示/\a -> (F a -> G a) -> X
。 /\
是类型级别的 lambda。也就是说,对于 a
的 特定 选择,调用者可以传入单个类型 a
和类型 F a -> G a
的函数。
(forall a. F a -> G a) -> X
表示 (/\a -> F a -> G a) -> X
并且调用者必须传入一个函数,被调用者 可以专门针对 many 选择 a
.
要真正回答显然继续让您迷惑的标题问题:Haskell 在您不提供显式签名时,总是为函数选择最通用的 rank-1 类型。因此对于表达式 app (runDB' pool)
中的 app
,GHC 将尝试使用类型
app :: DBRunner ActionM a -> ScottyM ()
这实际上是 shorthand
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类型
我正在使用 scotty
和 persistent
在 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
其中 getPeople
和 getFoods
是适当的 persistent
数据库操作,分别是 return [Person]
和 [Food]
。
一段时间后,在池中调用 liftIO
和 runSqlPool
的模式变得令人厌烦 - 如果我可以将它们重构为单个函数,那不是很好吗,就像 Yesod 的 runDB
,这只会采用查询和 return 适当的类型。我尝试写这样的东西是:
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
但它可以与 return 许多不同类型的数据库查询一起使用,就像我最初所做的那样。我认为我对这里的类型有一些基本的误解,但我不知道如何找出它是什么!任何帮助将不胜感激。
编辑:
根据 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 ()
对此我很感激,但仍然感到困惑!
来玩游戏吧:
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
给出很好的解释,所以我什至不会尝试。网络资源充足
It seems like GHC is saying that in fact the type of runDB becomes specialised somehow.
你猜对了。您的原始类型是 app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM ()
。这意味着您的 runDB
类型 SqlPersistT IO a -> m a
参数可以用于任何 one 类型 a
。但是,app
的主体想要在两种不同的类型(Person
和 Food
)中使用 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
,表示/\a -> (F a -> G a) -> X
。 /\
是类型级别的 lambda。也就是说,对于 a
的 特定 选择,调用者可以传入单个类型 a
和类型 F a -> G a
的函数。
(forall a. F a -> G a) -> X
表示 (/\a -> F a -> G a) -> X
并且调用者必须传入一个函数,被调用者 可以专门针对 many 选择 a
.
要真正回答显然继续让您迷惑的标题问题:Haskell 在您不提供显式签名时,总是为函数选择最通用的 rank-1 类型。因此对于表达式 app (runDB' pool)
中的 app
,GHC 将尝试使用类型
app :: DBRunner ActionM a -> ScottyM ()
这实际上是 shorthand
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类型