Michael 在他的博客文章中已经给出了很好的解释,但我将尝试用一个(人为的,但相对较小的)示例来说明它。
我们需要以下扩展:
{-# LANGUAGE FlexibleInstances, TypeFamilies #-}
让我们定义一个比 简单的类,CanFilter
只有一个参数。我正在定义该类的两个副本,因为我想演示两个实例之间的行为差异:
class Twice1 f where
twice1 :: f -> f
class Twice2 f where
twice2 :: f -> f
现在,让我们为每个类定义一个实例。对于Twice1
,我们直接将类型变量固定为相同,对于Twice2
,我们允许它们不同,但添加一个等式约束。
instance Twice1 (a -> a) where
twice1 f = f . f
instance (a ~ b) => Twice2 (a -> b) where
twice2 f = f . f
为了显示差异,让我们定义另一个重载函数,如下所示:
class Example a where
transform :: Int -> a
instance Example Int where
transform n = n + 1
instance Example Char where
transform _ = 'x'
现在我们已经到了可以看到差异的地步。一旦我们定义
apply1 x = twice1 transform x
apply2 x = twice2 transform x
并向 GHC 询问推断的类型,我们得到了
apply1 :: (Example a, Twice1 (Int -> a)) => Int -> a
apply2 :: Int -> Int
这是为什么?好吧,Twice1
仅当函数的源类型和目标类型相同时才会触发实例。对于transform
给定的上下文,我们不知道。GHC 只会在右侧匹配时应用实例,所以我们留下了未解析的上下文。如果我们尝试说apply1 0
,则会出现类型错误,表示仍然没有足够的信息来解决重载问题。Int
在这种情况下,我们必须明确指定要通过的结果类型。
但是,在 中Twice2
,实例适用于任何函数类型。GHC 会立即解决它(GHC 永远不会回溯,所以如果一个实例明显匹配,它总是被选中),然后尝试建立先决条件:在这种情况下,相等约束,然后强制结果类型为Int
,并允许我们也解决Example
约束。我们可以说apply2 0
没有进一步的类型注释。
所以这是关于 GHC 的实例解析的一个相当微妙的点,这里的等式约束有助于 GHC 的类型检查器以一种需要用户更少类型注释的方式进行。