【问题标题】:Which dictionary does GHC choose when more than one is in scope?当范围内有多个字典时,GHC 会选择哪个字典?
【发布时间】:2015-04-09 03:09:43
【问题描述】:

考虑以下示例:

import Data.Constraint

class Bar a where
  bar :: a -> a

foo :: (Bar a) => Dict (Bar a) -> a -> a
foo Dict = bar

在选择foo 中的Bar 实例时,GHC 有两个选择用于字典:它可以使用foo 上的Bar a 约束中的字典,或者它可以使用运行时Dict 来拿一本字典。有关字典对应于 不同 实例的示例,请参见 this question

GHC 使用哪个字典,为什么它是“正确”的选择?

【问题讨论】:

    标签: haskell ghc


    【解决方案1】:

    它只选择一个。这不是正确的选择;这是一个非常有名的疣。您可能会以这种方式导致崩溃,因此这是一种非常糟糕的情况。这是一个只使用 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,但这不是一个稳定的结果。把它变成崩溃留给读者。

    【讨论】:

    • 您能否详细说明如何导致崩溃?
    • @chi 我给出了一个 sn-p,它显示了如何在范围内获取两个不同的实例。从那里开始就不难了——只要在你的代码中假设所有实例都是平等的,你就会遇到问题。
    • 我认为您不会因此而导致崩溃,除非您计算由于破坏假设实例是一致的模块中的不变量而导致的崩溃,并使用此假设来证明 unsafeCoerce 或类似。 (但我会说崩溃是由unsafeCoerce引起的。)
    • 具体来说,有人可能会认为,虽然混淆 TrueFalse 对原本有效的 Haskell 程序是无害的,但可以使用关联类型来减少 Int 和 @987654330 之间的无害混淆@。但实际上 GHC 似乎总是检查类型族实例是否有重叠。
    • @DanielWagner 很有趣。我认为禁止导入定义相同类型的两个实例的两个模块。可能检查会延迟使用,如果不是 GADT 就可以了...
    【解决方案2】:

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

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

    简而言之,如果你使用 OverlappingInstances,你就放弃了在这里询问将选择哪个字典的问题。

    现在确实可以在不重叠实例的情况下破坏实例的一致性。实际上,您可以在没有孤儿和除 FlexibleInstances 以外的任何扩展的情况下做到这一点(可以说问题是在启用 FlexibleInstances 时“孤儿”的定义是错误的)。这是一个长期存在的 GHC 错误,部分原因是(a)它实际上不能直接导致崩溃,据任何人似乎都知道,并且(b)可能有很多程序实际上依赖于在程序的不同部分中为同一约束提供多个实例,这可能很难避免。

    回到主题,原则上重要的是 GHC 可以选择任何可用的字典来满足约束,因为即使它们应该相等,GHC 可能比其中一些具有更多的静态信息其他。您的示例有点过于简单,无法说明,但假设您将参数传递给bar;一般来说,GHC 对通过 Dict 传入的字典一无所知,因此它必须将此视为对未知函数的调用,但您在特定类型 T 上调用了 foo,其中有一个 @ 987654325@ 实例在范围内,然后 GHC 会知道来自Bar a 约束字典的barTbar 并且可以生成对已知函数的调用,并且可能内联T 的@ 987654331@ 并因此进行更多优化。

    实际上,GHC 目前还没有这么聪明,它只是使用可用的最里面的字典。总是使用最外层的字典可能已经更好了。但是像这种有多个字典可用的情况并不常见,所以我们没有很好的基准测试。

    【讨论】:

    • 有人可能会问为什么我们有两个扩展(不连贯/重叠)而不是一个。人们可能会觉得单独重叠仍然是“安全的”,只有在语无伦次的情况下,真正的麻烦才开始。
    • "同一约束的任何两个字典都应该相等。" ——应该是,但不是。有关在不使用不连贯或重叠实例的情况下违反此声明的示例,请参阅我的答案。
    • 是的,这就是我所说的失去实例一致性的意思。
    【解决方案3】:

    这是一个测试:

    {-# 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 碰巧使用了被纳入范围的“最后一个”字典。不过,我不会依赖这个。

    如果您将自己限制为重叠实例,那么您将无法为同一类型引入两个不同的字典(据我所知),一切都应该没关系,因为字典的选择变得无关紧要。

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

    简而言之,不连贯的实例是邪恶的。


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

    【讨论】:

    • 我知道IncoherentInstances 会导致各种麻烦,这就是我的示例使用它的原因(但它确实使用了OverlappingInstances and an orphan instance in another module). I didn't realize OverlappingInstances ` 也可能导致未定义的行为...我认为它是“安全的”。
    • @Eric 我也这么认为。也许它可以被认为是一个错误,但我不是这方面的专家。
    • @Eric 作为记录,仅使用重叠实例我可以让Dict 将“最佳”实例打包到模块中:即使没有“不连贯”扩展,GHC 也会提交它,因为只有一个最佳选择。在另一个模块中,我导入了它,并定义了一个更好的实例(这应该是不允许的,恕我直言)。
    • 你能更新你的例子吗?如果你有另一种方式使Overlapping 没有Incoherent 可能不安全,我会发布一个错误。
    • 我相信即使没有OverlappingInstances,也有可能欺骗 GHC,这是孤儿被认为是邪恶的一大原因。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-09
    • 2021-10-21
    • 1970-01-01
    相关资源
    最近更新 更多