我正在尝试构建一个基于
gitlib
的应用程序,并想使用此类型签名编写一些附加函数:MonadGit r m => m [RefName]
。它与 gitlib
自己的 listReferences
相同,所以我将使用它作为示例:
module Foo where
import Conduit
import Git.Types
listReferences :: MonadGit r m => m [RefName]
listReferences = runConduit $ sourceReferences .| sinkList
这将按预期使用
$ ghc Reference.hs
中的 nix-shell -p 'ghc.withPackages (p: with p; [ gitlib-libgit2 ])'
进行编译
但是,在同一个文件中,我还需要使用
Git.Libgit2
,我将其作为合格的导入,如下所示:
module Foo where
import Conduit
import Git.Types
import qualified Git.Libgit2 as LG
listReferences :: MonadGit r m => m [RefName]
listReferences = runConduit $ sourceReferences .| sinkList
这不再编译,抛出以下错误:
$ ghc Reference.hs
[1 of 1] Compiling Foo ( Reference.hs, Reference.o ) [Source file changed]
Reference.hs:7:19: error: [GHC-25897]
• Couldn't match type ‘r’ with ‘LG.LgRepo’
arising from a functional dependency between constraints:
‘MonadGit LG.LgRepo m’
arising from a type ambiguity check for
the type signature for ‘listReferences’ at Reference.hs:7:19-45
‘MonadGit r m’
arising from the type signature for:
listReferences :: forall r (m :: * -> *).
MonadGit r m =>
m [RefName] at Reference.hs:7:19-45
‘r’ is a rigid type variable bound by
the type signature for:
listReferences :: forall r (m :: * -> *).
MonadGit r m =>
m [RefName]
at Reference.hs:7:19-45
• In the ambiguity check for ‘listReferences’
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
In the type signature:
listReferences :: MonadGit r m => m [RefName]
|
7 | listReferences :: MonadGit r m => m [RefName]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
这里究竟发生了什么;为什么它试图将该类型与我什至没有在任何地方使用的其他类型相匹配? (Linter 确认导入未使用。)
如何解决这个问题?
这是“简单”的答案:通过导入模块
Git.Libgit2
,无论是否合格,你都在选择r ~ LG.LgRepo
,所以只需给GHC它想要的:
listReferences :: (MonadGit LG.LgRepo m) => m [RefName]
listReferences = runConduit $ sourceReferences .| sinkList
这可能会引发有关脆弱绑定的警告,您可以通过打开
MonoLocalBinds
扩展来解决此问题(如果您使用 GADTs
或 TypeFamilies
,无论如何都会打开该扩展):
{-# LANGUAGE MonoLocalBinds #-}
或者通过有问题的实例取消约束来直接解决问题,这是非常恶心的:
import qualified Git.Libgit2 as LG
import qualified Git.Libgit2.Types as LG
listReferences :: (LG.MonadExcept m, MonadUnliftIO m, LG.HasLgRepo m) => m [RefName]
listReferences = runConduit $ sourceReferences .| sinkList
无论如何,以下模块应该进行类型检查:
{-# LANGUAGE GHC2021, MonoLocalBinds #-}
module Foo where
import Conduit
import Git.Types
import qualified Git.Libgit2 as LG
listReferences :: MonadGit LG.LgRepo m => m [RefName]
listReferences = runConduit $ sourceReferences .| sinkList
这是更长的答案...
这是实例的全局范围、类型依赖性和歧义性检查之间的奇怪交互。
在 Haskell 中,实例没有作用域——它们都在相同的“实例的全局作用域”中浮动。因此,如果您足够努力,您可以构造一个示例,其中模块的限定导入将一个新实例引入到此全局范围中,从而破坏编译,即使限定导入中的定义实际上没有被“使用”。
在这种特定情况下,
MonadGit
类在Git.Types
中定义,具有函数依赖性:
class ... => MonadGit r m | m -> r where
据我所知,gitlib
包中没有
实例,因此当 GHC 尝试输入时检查签名:
listReferences :: MonadGit r m => m [RefName]
仅导入 Git.Types
,所要做的就是类型类。现在,如果没有函数依赖性,此类型签名将是不明确的,因为类型
r
出现在约束中,但不在后面的任何“可见”类型中。然而,由于函数依赖
m -> r
,GHC 得出结论,这种类型没有歧义,因为 - 在特定
listReferences
处调用
m
后,函数依赖将确定正确的
r
。但是,当您添加
Git.Libgit2
的限定导入时,它将以下实例带入范围:
instance ... => Git.MonadGit LgRepo m where
因为这个实例适用于所有 m
,所以它以一种微不足道的方式遵循函数依赖性——
m
的每个选择都意味着
r ~ LgRepo
。所以,GHC 在这个实例上没有问题。不幸的是,这确实会导致歧义性检查出现问题。当 GHC 试图确定
r
是否有歧义时,它最终会推理出——因为作用域中的新实例通过函数依赖表明,对于
m
的任何选择,我们必须有
r ~ LgRepo
——
r
的唯一有效类型是
r ~ LgRepo
,因此它尝试统一
r
和
LgRepo
并最终抛出类型检查错误,因为
r
是一种“刚性”类型,可以不要这样统一。您可能认为这是一个错误或错误的行为,或者 GHC 应该更了解,但事实就是如此。
如上所述,解决此问题的最简单方法是将
r
专门化为
LgRepo
,当
r
是程序的一部分时,这是唯一可能的
Git.Libgit2
。