当范围内有多个词典时,GHC会选择哪一个词典



考虑以下示例:

import Data.Constraint
class Bar a where
  bar :: a -> a
foo :: (Bar a) => Dict (Bar a) -> a -> a
foo Dict = bar

GHC在foo中选择Bar实例时有两个选项可供字典使用:它可以使用fooBar a约束中的字典,也可以使用运行时Dict来获取字典。有关字典对应不同实例的示例,请参阅此问题。

GHC使用哪本词典,为什么它是"正确"的选择?

它只选择一个。这不是正确的选择;这是一个很有名的疣。你可以通过这种方式造成崩溃,所以这是一种非常糟糕的情况。以下是一个只使用GADTs的简短示例,它表明可以同时在作用域中有两个不同的实例:

-- file Class.hs
{-# LANGUAGE GADTs #-}
module Class where
data Dict a where
  Dict :: C a => Dict a
class C a where
  test :: a -> Bool
-- file A.hs
module A where
import Class
instance C Int where
  test _ = True
v :: Dict Int
v = Dict
-- file B.hs
module B where
import Class
instance C Int where
  test _ = False
f :: Dict Int -> Bool
f Dict = test (0 :: Int)
-- file Main.hs
import TestA
import TestB
main = print (f v)

您会发现Main.hs编译得很好,甚至可以运行。它用GHC 7.10.1在我的机器上打印True,但这不是一个稳定的结果。把这件事变成一场崩溃就留给读者了。

GHC只选择了一个,这是正确的选择。同一约束的任意两个字典应该是相等的。

重叠实例和非相干实例的破坏力基本相等;它们在设计上都失去了实例一致性(程序中的任何两个相等约束都由同一字典满足)。OverlappingInstances使您能够根据具体情况确定哪些实例将被使用,但当您将Dicts作为第一类值传递时,这并没有那么有用,等等。只有当我认为重叠实例在扩展上等效时,我才会考虑使用OverlappingInstances(例如,对于像Int这样的特定类型,一个更高效但在其他方面相等的实现),但即便如此,如果我足够关心性能来编写专门的实现,如果它在可能的时候没有被使用,这不是一个性能缺陷吗?

简而言之,如果你使用OverlappingInstances,你就放弃了在这里选择哪本词典的权利。

现在,您可以在没有OverlappingInstances的情况下打破实例一致性。事实上,您可以在没有孤立项和FlexibleInstances之外的任何扩展的情况下执行此操作(可以说,问题是启用FlexibleInstance时"孤立项"的定义是错误的)。这是一个长期存在的GHC错误,尚未修复,部分原因是(a)据任何人所知,它实际上不会直接导致崩溃,以及(b)可能有很多程序实际上依赖于在程序的不同部分中为同一约束拥有多个实例,这可能很难避免。

回到主题,原则上,GHC可以选择任何可用的字典来满足约束是很重要的,因为即使它们应该相等,GHC可能比其他字典拥有更多关于它们的静态信息。您的示例有点过于简单,无法说明问题,但假设您将一个参数传递给bar;一般来说,GHC对通过Dict传递的字典一无所知,因此它必须将其视为对未知函数的调用,但您在特定类型T调用了foo,该类型在作用域中有一个Bar T实例,那么GHC就会知道Bar a约束字典中的barTbar,并且可以生成对已知函数的调用,以及潜在地内联CCD_ 18的CCD_。

在实践中,GHC目前并没有那么聪明,它只是使用最内部的可用字典。最好总是使用最外层的字典。但是像这样有多个字典可用的情况并不常见,所以我们没有很好的基准测试。

这里有一个测试:

{-# LANGUAGE FlexibleInstances, OverlappingInstances, IncoherentInstances #-}
import Data.Constraint
class    C a    where foo :: a -> String
instance C [a]  where foo _ = "[a]"
instance C [()] where foo _ = "[()]"
aDict :: Dict (C [a])
aDict = Dict
bDict :: Dict (C [()])
bDict = Dict
bar1 :: String
bar1 = case (bDict, aDict :: Dict (C [()])) of
         (Dict,Dict) -> foo [()]              -- output: "[a]"
bar2 :: String
bar2 = case (aDict :: Dict (C [()]), bDict) of
         (Dict,Dict) -> foo [()]              -- output: "[()]"

上述GHC恰好使用了被纳入范围的"最后一个"字典。不过,我不会相信这一点。

如果您仅限于重叠的实例,那么您将无法为同一类型引入两个不同的字典(据我所见),并且一切都应该很好,因为字典的选择变得无关紧要。

然而,不连贯的实例是另一个野兽,因为它们允许您提交到一个通用实例,然后在具有更特定实例的类型中使用它。这使得很难理解将使用哪个实例。

简而言之,语无伦次的事例是邪恶的。


更新:我进行了一些进一步的测试。在单独的模块中仅使用重叠实例和孤立实例,仍然可以获得相同类型的两个不同字典。因此,我们需要更多的注意事项-(

最新更新