关于haskell:定义类(* -> *)时(Eq,Show)重叠实例问题的通用解决方案

Generic solution to (Eq, Show) overlapping instances issue when defining class (* -> *)

Stack 在重叠实例上有很多线程,虽然这些有助于解释问题的根源,但我仍然不清楚如何重新设计我的代码以使问题消失。虽然我肯定会投入更多的时间和精力来研究现有答案的细节,但我将在这里发布我确定为造成问题的一般模式,希望存在一个简单而通用的答案:我通常会发现自己定义一个类,例如:

1
2
3
4
{-# LANGUAGE FlexibleInstances #-}
class M m where
  foo :: m v -> Int
  bar :: m v -> String

连同实例声明:

1
2
3
4
5
instance (M m) => Eq (m v) where
  (==) x y = (foo x) == (foo y)      -- details unimportant

instance (M m) => Show (m v) where
  show = bar                         -- details unimportant

在我的工作过程中,我不可避免地会创建一些数据类型:

1
data A v = A v

并将 A 声明为类 M:

的实例

1
2
3
instance M A where
  foo x = 1                           -- details unimportant
  bar x ="bar"

然后定义A Integer的一些元素:

1
2
x = A 2
y = A 3

打印 xy 或评估布尔 x == y 没有问题,但如果我尝试打印列表 [x] 或评估布尔 [x] == [y],则会发生重叠实例错误:

1
2
3
4
5
6
main = do
 print x                                     -- fine
 print y                                     -- fine
 print (x == y)                              -- fine
 print [x]                                   -- overlapping instance error
 if [x] == [y] then return () else return () -- overlapping instance error

我认为这些错误的原因现在非常清楚:它们源于现有的实例声明 instance Show a => Show [a]instance Eq a => Eq [a],虽然确实 [] :: * -> * 还没有被声明为我的类的实例 M,没有什么可以阻止某人在某些时候这样做:因此编译器会忽略实例声明的上下文。

当面对我所描述的模式时,如何重新设计它来避免问题?


在实例搜索中没有回溯。实例完全基于实例头部的句法结构进行匹配。这意味着在实例解析期间不考虑实例上下文。

所以,当你写

1
2
instance (M m) => Show (m v) where
    show = bar

您是在说"这是 Show 的一个实例,适用于任何类型的 m v"。由于 [x] :: [] (A Int) 确实是 m v 形式的类型(设置 m ~ []v ~ A Int),因此对 Show [A Int] 的实例搜索会出现两个候选:

1
2
instance Show a => Show [a]
instance M m => Show (m v)

就像我说的那样,类型检查器在选择实例时不会查看实例的上下文,因此这两个实例是重叠的。

解决方法是不声明像 Show (m v) 这样的实例。作为一般规则,声明其头部纯粹由类型变量组成的实例是一个坏主意。您编写的每个实例都应该从一个诚实的类型构造函数开始,并且您应该怀疑不符合该模式的实例。

为您的默认实例提供 newtype 是一种相当标准的设计(例如,参见 WrappedBifunctor\\ 的 Functor 实例),

1
2
3
4
newtype WrappedM m a = WrappedM { unwrapM :: m a }

instance M m => Show (WrappedM m a) where
    show = bar . unwrapM

as 在顶层给出函数的默认实现(参见例如 foldMapDefault):

1
showDefault = bar