从错误中学习:`overlapping instances`实例

158 阅读13分钟

并非所有的GHC错误都是天生平等的。其中一些很容易追踪和修复,而另一些则不然。有些错误的变种可以跨越整个范围。

在这篇文章中,我们将看一下overlapping instances 错误。我们将了解它的许多变体,以及该错误在每种情况下意味着什么。在这个过程中,我们可能还会学到一些关于GHC行为的有趣和高级的东西。

简单的重叠实例

让我们看看这个错误的一个基本版本,它是由以下代码触发的。

{-# LANGUAGE FlexibleInstances #-}

class Printable a where
  printMe :: a -> IO ()

instance Printable a where
  printMe a = putStrLn "dummy instance"

instance Printable Int where
  printMe x = putStrLn ("I am an Int with value :" ++ show x)

main :: IO ()
main = printMe (5 :: Int)

这给了我们下面的错误:

• Overlapping instances for Printable Int
    arising from a use of ‘printMe’
  Matching instances:
    instance Printable a -- Defined at app/Main.hs:8:10
    instance Printable Int -- Defined at app/Main.hs:11:10
• In the expression: printMe (5 :: Int)

注意,这个错误发生在有调用printMe 函数的地方。如果没有对该函数的调用,就不会出现这个错误。

重叠实例错误是由实例搜索触发的,而不是由实例声明触发的。你可以在你的代码中放入各种重叠的实例,GHC不会对此视而不见,直到你触发实例搜索,比如在调用一个类型的方法。

让我们看看在调用站点printMe (5 ::Int) 中发生了什么。 我们在范围内有两个匹配的实例。一般的实例,Printable a ,和一个特定的实例,用于Int 。我们称它为 "一般实例",因为它可以匹配任何类型,而针对Int 的实例只能匹配Int 类型。

在这里,GHC选择了一个错误,而不是使用更具体的Int的实例。这种行为可以帮助程序员不至于错误地覆盖一个通用实例。当两个实例都在同一个模块中时,这很容易被发现,但是如果一般的实例在另一个模块中,或者在不同的包中呢?悄悄地覆盖一个现有的实例,或者在添加一个新的实例时没有注意到一个现有的实例,都会以微妙的方式破坏程序。

如何解决这个问题?

解决这个问题的一个方法是让GHC知道,在有其他匹配实例的情况下选择实例是没有问题的。我们通过使用OVERLAPPING pragma来做到这一点。比如说

instance Printable a where
  printMe a = putStrLn "dummy instance"

instance {-# OVERLAPPING #-} Printable Int where
  printMe x = putStrLn ("I am an Int with value :" ++ show x)

完整的代码

  {-# LANGUAGE FlexibleInstances #-}

  class Printable a where
    printMe :: a -> IO ()

  instance Printable a where
    printMe a = putStrLn "dummy instance"

  instance {-# OVERLAPPING #-} Printable Int where
    printMe x = putStrLn ("I am an Int with value :" ++ show x)

  main :: IO ()
  main = printMe (5 :: Int)

我们也可以通过使用OVERLAPPABLE pragma将一般实例标记为可安全覆盖,如下所示。

instance {-# OVERLAPPABLE #-} Printable a where
  printMe a = putStrLn "dummy instance"

instance Printable Int where
  printMe x = putStrLn ("I am an Int with value :" ++ show x)

完整的代码

  {-# LANGUAGE FlexibleInstances #-}

  class Printable a where
    printMe :: a -> IO ()

  instance {-# OVERLAPPABLE #-} Printable a where
    printMe a = putStrLn "dummy instance"

  instance Printable Int where
    printMe x = putStrLn ("I am an Int with value :" ++ show x)

  main :: IO ()
  main = printMe (5 :: Int)

如果你想把一个实例标记为可重写,以及它能够安全地重写其他实例,你可以使用OVERLAPS pragma。所以在我们的例子中,你可以在其中任何一个实例中使用OVERLAPS pragma,它就会工作。

请注意,OVERLAPPING pragma只是意味着使用该实例是正确的(即使有其他更通用的实例),而不是一个明确的指令,即优先使用该实例。

与类型变量重叠的实例

在这里,我们稍微改变了上面的一个修复方法,通过另一个接受多态参数的函数fn ,来调用printMe 函数。

fn :: a -> IO ()
fn x = printMe x

完整的代码

{-# LANGUAGE FlexibleInstances    #-}

class Printable a where
  printMe :: a -> IO ()

instance Printable a where
  printMe a = putStrLn "general instance"

instance {-# OVERLAPPING #-} Printable Int where
  printMe a = putStrLn "int instance"

fn :: a -> IO ()
fn x = printMe x

main :: IO ()
main = fn (5 :: Int)

看吧,可怕的错误又出现了。

   • Overlapping instances for Printable a
       arising from a use of ‘printMe’
     Matching instances:
       instance [overlappable] Printable a -- Defined at app/Main.hs:8:32
       instance Printable Int -- Defined at app/Main.hs:11:10
     (The choice depends on the instantiation of ‘a’
      To pick the first instance above, use IncoherentInstances
      when compiling the other instance declarations)

在这里,我们在错误信息中得到了一些额外的信息。它说'选择取决于'a'的实例'为了挑选上面的第一个实例,使用IncoherentInstances'。

所以发生这种情况是因为在printMe x 的调用地点,GHC只知道x 可以是任何类型,包括Int 。在不知道a 是否是Int 的情况下,它不能挑选最具体的实例,从而导致了这个错误。

如何解决这个问题?

解决这个问题的正确方法无非是普通的类型约束。

因此,如果你在fn 中添加一个Printable a 约束,那么适当的实例就会从调用站点传递过来,并且在fn 中的调用可以使用这个实例。

因此,这就是这种情况下的正确修复方法。在fn 中添加一个Printable a 约束。

fn :: Printable a => a -> IO ()
fn x = printMe x

完整的代码

{-# LANGUAGE FlexibleInstances #-}

class Printable a where
  printMe :: a -> IO ()

instance Printable a where
  printMe a = putStrLn "general instance"

instance {-# OVERLAPPING #-} Printable Int where
  printMe a = putStrLn "int instance"

main :: IO ()
main = fn (5 :: Int)

fn :: Printable a => a -> IO ()
fn x = printMe x

当然,另一个修复方法是将Int 实例标记为INCOHERENT 。 这将会选择在调用网站上有信息的实例,即使有不同的实例可用,并且如果有更多的信息,也会被选中。

instance {-# INCOHERENT #-} Printable Int where
  printMe a = putStrLn "int instance"

完整的代码

{-# LANGUAGE FlexibleInstances #-}

class Printable a where
  printMe :: a -> IO ()

instance Printable a where
  printMe a = putStrLn "general instance"

instance {-# INCOHERENT #-} Printable Int where
  printMe a = putStrLn "int instance"

main :: IO ()
main = fn (5 :: Int)

fn :: a -> IO ()
fn x = printMe x

这里描述了实例解析算法所遵循的规则,在这个特定的案例中,一般的实例将被应用,因为它最终是唯一的 "主要候选者",被选中,因为其余的实例被标记为INCOHERENT 。这意味着,如果你运行该程序,它将打印出 "一般实例"。

短视的GHC "重叠的实例

这里我们看一下这个错误的一个变种,人们觉得GHC有时非常短视。

{-# LANGUAGE FlexibleInstances #-}

import Data.Typeable

class Printable a where
  printMe :: a -> IO ()

instance Printable a where
  printMe _ = putStrLn "General instance"

instance Functor f => Printable (f a) where
  printMe _ = putStrLn "Instance for a Functor"

newtype MyType a = MyType a

main :: IO ()
main = printMe (MyType 5)

正如所料,我们得到了重叠实例的错误。

• Overlapping instances for Printable (MyType Char)
    arising from a use of ‘printMe’
  Matching instances:
    instance Printable a -- Defined at app/Main.hs:10:10
    instance Functor f => Printable (f a)
      -- Defined at app/Main.hs:13:10

由于我们在上一个例子中看到了一个类似的错误,并且通过移除其中一个实例而解决了这个问题,所以看起来同样的方法在这里也可以起作用。

而且这也是有道理的。既然GHC被两个符合条件的实例所迷惑,那么很有可能,删除其中一个就能解决这个问题,对吗?

因此,我们尝试删除Printable a 的实例。

显示改变后的代码

{-# LANGUAGE FlexibleInstances #-}

import Data.Typeable

class Printable a where
  printMe :: a -> IO ()

instance Functor f => Printable (f a) where
  printMe _ = putStrLn "Instance for a Functor"

newtype MyType a = MyType a

main :: IO ()
main = printMe (MyType 'c')

当我们重新编译时,我们得到...

• No instance for (Functor MyType) arising from a use of ‘printMe’
• In the expression: printMe (MyType 'c')
  In an equation for ‘main’: main = printMe (MyType 'c')

令我们惊讶的是,我们发现从两个符合条件的实例中删除一个实例,似乎让GHC更仔细地研究了剩下的实例,最后它拒绝了之后的实例。看来,GHC在宣布这些实例为多余的之前,第一次并没有对它们进行足够的观察。

让我们走过这个算法,看看为什么第一个错误会发生。

所以第一步是:

找到所有符合目标约束的实例II I;也就是说,目标约束是II I的替换实例。这些实例的声明就是候选者。

我们这里的目标约束是MyType Char ,而Printable aPrintable (f a) 的实例都符合。

下一步说:

如果没有候选者,则搜索失败。

由于我们有两个匹配的实例,我们可以继续进行下一步,即:

排除任何候选人IXIX IX,因为它有另一个候选人IYIY IY,使得以下两个条件都成立。IYIY IY严格地比IXIX IX更具体。也就是说,IYIY IY是IXIX IX的一个替换实例,但反之则不是。要么IXIX IX是可重叠的,要么IYIY IY是重叠的。(这种 "非此即彼 "的设计,而不是 "兼而有之 "的设计,允许客户故意覆盖一个库中的实例,而不需要对该库进行修改)。

我们有两个候选人,Printable aPrintable (f a) 。让我们先处理Printable a 。规则说要检查是否有另一个候选的IYIY IY,使得IYIY IY是IXIX IX的一个替换实例。这里f aa 的一个替换实例,因为如果某个东西可以接受a ,它也可以接受f a ,但不能反过来说。所以它符合,规则的下一部分说,要么Printable a 是可以重叠的,要么Printable (f a) 应该是重叠的。既然情况不是这样,我们就不能排除Printable a

下一个是Printable (f a) ,我们不能消除它,因为另一个实例a 不是f a 的替代实例。如果有东西在期待f a,你不能把a 给它。或者换句话说,a 并不比f a 更具体,相反,它更具有一般性。

因此,在这条规则之后,这两个实例仍然存在,而下一条规则说:

如果所有剩下的候选者都是不连贯的,则搜索成功,返回一个任意的存活的候选者。

我们的实例中没有一个是用{-# INCOHERENT #-} pragma标记的,所以我们继续下一条规则:

如果有超过一个不连贯的候选者,搜索就会失败。

考虑到我们现在有两个这样的实例,搜索在此失败。

让我们尝试在实例中添加一个OVERLAPPING pragma,用于f a

instance {-# OVERLAPPING #-} Functor f => Printable (f a) where
  printMe _ = putStrLn "Instance for a Functor"

现在我们得到了这个错误。

• No instance for (Functor MyType) arising from a use of ‘printMe’
• In the expression: printMe (MyType 'c')
  In an equation for ‘main’: main = printMe (MyType 'c')

我们可以看到,添加OVERLAPPING pragma 使得在步骤 3 中消除了Printable a 的实例。但是剩下的实例Functor f => Printable (f a) 没有成功,因为MyType 不是Functor 。但是这个失败发生在一个较晚的阶段:当约束条件被匹配时,在 GHC 选择了一个实例之后。这就是为什么我们会得到一个No instance for Functor MyType的错误,而不是一个重叠的实例错误。

如果我们删除一般的实例instance Printable a ,也会发生类似的情况。剩下的实例将在上下文匹配步骤中被拒绝。

在这里,我们遇到了实例解析的一个重要方面:该算法在两个不同的步骤中工作,而且它从不回溯。

在第一步中,它完全不看约束条件,只看实例头。

因此,与其说是:

instance Printable a
instance Functor f => Printable (f a)

它看到:

instance Printable a
instance ... => Printable (f a)

在下一个步骤中,约束条件被匹配。

所以在这个例子中,当我们添加一个OVERLAPPING pragma时,它使第一步成功完成,结果是实例Functor f => Printable (f a) 。但是在上下文匹配步骤中,这个实例失败了,因为MyType 不是一个Functor

因为GHC不会回溯,所以它不会带着对这次失败的记忆回到第一步,并选择一般的实例。理解这两步的无回溯行为,对于解决大多数这种错误的发生至关重要。

如何解决这个问题?

我们可以删除f a 的实例,这使得算法挑选Printable a 的实例。或者,如果有意义的话,我们可以为MyType a添加一个 Functor 实例。

多种类的重叠实例

这是重叠实例错误的一个版本,只发生在PolyKinds 语言扩展和随之而来的自动种类推理中。

为了证明这一点,我们很不幸地需要一个更复杂的设置,坦率地说,这个例子有点矫揉造作。总之,我们有下面这段代码,它触发了我们心爱的错误。

{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures         #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE PolyKinds              #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE UndecidableInstances   #-}

import Data.Proxy
import GHC.TypeLits

class Printable n a where
  printMe :: Proxy n -> a -> IO ()

class SomeClass (n :: Symbol) a where

instance SomeClass n (Maybe Char) where

instance Printable n a where
  printMe p a = putStrLn "General instance"

instance {-# OVERLAPPING #-} SomeClass n (Maybe a) => Printable n (Maybe a) where
  printMe p a = putStrLn "Specific instance"

fn :: Proxy n -> Maybe Char -> IO ()
fn p a = printMe p a

main :: IO ()
main = fn Proxy (Just 'c')

正如你所看到的,除了启用一堆语言扩展之外,我们还在printMe 方法中添加了一个Proxy 参数。除了触发和演示错误外,它没有其他作用。

这里我们有这两个实例:

instance Printable n a where
  printMe p a = putStrLn "General instance"

instance {-# OVERLAPPING #-} SomeClass n (Maybe a) => Printable n (Maybe a) where
  printMe p a = putStrLn "Specific instance"

根据我们已经看到的,在调用printMe p (Just 'c') 时应该选择第二个实例,因为它有OVERLAPPINGpragma,而且它似乎是更具体的实例。

但尽管如此,这里还是出现了错误。

||     • Overlapping instances for Printable n (Maybe Char)
||         arising from a use of ‘printMe’
||       Matching instances:
||         instance forall k (n :: k) a. Printable n a
||           -- Defined at app/Main.hs:22:10
||         instance [overlapping] SomeClass n (Maybe a) =>
||                                Printable n (Maybe a)
||           -- Defined at app/Main.hs:25:30
||       (The choice depends on the instantiation of ‘k, n’
||        To pick the first instance above, use IncoherentInstances
||        when compiling the other instance declarations)
||     • In the expression: printMe p a
||       In an equation for ‘fn’: fn p a = printMe p a

让我们仔细看一下第二个实例。

instance {-# OVERLAPPING #-} SomeClass n (Maybe a) => Printable n (Maybe a) where

这里看起来n 的种类可以是任何种类,但是PolyKinds的扩展和约束SomeClass n (Maybe a) 导致种类推理系统推断出n 的类型必须是Symbol 。而在调用的地方,在fn 函数中,我们不知道n 的种类。如果是Symbol ,那么应该调用第二个实例,但如果是其他种类,那么应该调用第一个实例。这个难题让GHC放弃了,并产生了这个错误。

如何解决这个问题呢?

我们可以看到,一旦我们从第二个实例中移除SomeClass n (Maybe a)的约束,错误就会消失。另外,我们也可以保留这个约束,并从调用地点对代理进行亲切的注释。例如,对调用站点的以下改变将修复错误并调用第一个(一般)实例。

fn :: Proxy (n :: *) -> Maybe Char -> IO ()
fn p a = printMe p a

而下面的内容将修复它并调用第二个(特定)实例。

fn :: Proxy (n :: Symbol) -> Maybe Char -> IO ()
fn p a = printMe p a

总结

在这里,我们看到了一些常见的overlapping instances 错误的实例,GHC似乎很喜欢时不时地给我们展示一下。希望我们已经了解了一两件关于GHC如何解决类型类实例的事情,这可能会帮助我们在下次遇到这个错误时正确地追踪和修复它。