standaloneBar 并不真正具有相同的类型。它是a -> b -> a,与类 中为bar 指定的类型相同。但问题不在于bar 3 (Baz 4) 未能匹配类中的通用类型,而是bar 3 (Baz 4) 未能唯一确定一个实例。
当涉及到类型类时,类型推断不仅要确定存在一些类型良好的类型变量赋值,它还必须实际决定选择哪个特定实例1。不同的实例可能有非常不同的行为,所以选择很重要。
编译器推断在您的代码中使用bar 的类型是bar :: (Foo a (Baz b), Num a, Num b, Show a) => a -> Baz b -> a(Show 约束来自传递给show 的结果)。现在,来自Foo Int (Baz a) 实例的bar 的版本具有Int -> Baz a -> Int 类型,这显然确实 与您对bar 的用法一致。但其他可能的情况也可能统一。可能有Foo Double (Baz a),或Foo a (Baz Float),或任何数量的其他可能性。
编译器可以通过选择Foo Int (Baz a) 来工作,因为这不是范围内的唯一实例,而且它确实适合。然而,语言规则的设计使得编译器在确定需要哪个实例时实际上不应该考虑范围内的实例!需要从调用的上下文中唯一清楚地清除适当的实例,然后编译器会检查这样的实例是否实际可用。所以需要有一个多态的Foo a (Baz b) 实例在此代码工作的范围内。事实上,如果我将您的实例替换为:
instance Foo a (Baz b) where
bar i _ = i
然后你的代码编译并运行没有错误!
因此,您的原始代码对类型不够具体,无法唯一确定 Foo Int (Baz b) 是必需的实例,因此编译器会报告有关不明确类型的错误。问题不在于您对bar 的使用与a -> b -> a 不统一,甚至与Int -> Baz b -> Int 不统一;它确实与两者统一。相反,问题在于它不是特定类型Int -> Baz b -> Int。添加额外的类型信息可以解决这个问题,并且是必需的。
做出这种语言设计决定的原因是,实例在范围内的变体(例如通过添加和删除导入)永远不会将有效代码的含义更改为其他有效代码。如果删除所需的实例,代码将停止工作,如果添加冲突的实例,代码将导致错误,但如果您的代码使用其中 1 个在范围内编译 2 个实例,则永远无法继续仅通过更改您的进口来工作和使用另一个。目的是实例的选择应该是程序员提供的代码固有的要求,而不应该只是编译器意外做出的选择。
值得注意的是,此实例选择规则有一个主要例外,它不是基于哪些实例实际上在范围内2。那就是类型默认。
在我上面描述的规则下,涉及数字文字的简单表达式几乎总是模棱两可的。一个例子是show $ 1 + 2。 + 和 show 对不同的实例有不同的行为(例如,+ 甚至不完全关联浮点数!),所以根据上面的推理,这段代码应该是无效的,程序员应该要求写show $ 1 + (2 :: Int)之类的东西。
语言设计者认为这过于繁琐,因此定义了默认规则。它们在the Haskell Report 中有更详细的描述,但基本上,如果存在涉及“数字”类型类之一的约束(如Num、Integral 等) Prelude 和二义性类型没有任何类型类约束除了那些涉及 Prelude 中定义的类。如果满足这些(非常保守的)约束,那么将尝试一些默认类型(默认类型的默认列表是Integer, Double,但可以自定义;但是无法自定义尝试默认的条件),如果其中之一允许为所有约束找到实例,那么编译器将接受代码并为您选择这些实例。
这意味着bar :: (Foo a (Baz b), Num a, Num b, Show a) => a -> Baz b -> a中的二义性类型变量a和b不能被默认;他们参与了约束Foo a (Baz b),这不适用于 Prelude 中定义的类。
但是当你使用standaloneBar(或者你使用我上面的更多态的Foo a (Baz b)实例)时,编译器无法解决的唯一约束是(Num a, Num b, Show a)。这里所有的约束都是 Prelude 类,而Num 是一个“数字类”,所以编译器会为a 和b 尝试Integer,这样可以工作并允许代码编译。
1除非类型类约束可以在调用者的签名中“传递”,但是这里的调用者是main :: IO (),所以这个选项不可用。
2如果你启用更多的扩展,比如OverlappingInstances,那么会有更多的例外,但我在这里不做描述。