为什么在Haskell中选择typeclass实例时不考虑上下文?

为什么在Haskell中选择typeclass实例时不考虑上下文?,haskell,typeclass,Haskell,Typeclass,我明白,当你 instance (Foo a) => Bar a instance (Xyy a) => Bar a GHC不考虑上下文,并且实例被报告为重复。 违反直觉的是,(我猜)在选择一个实例之后,它仍然需要检查上下文是否匹配,如果不匹配,则丢弃该实例。那么,为什么不颠倒顺序,放弃不匹配上下文的实例,继续处理剩余的集合呢 这会在某种程度上难以解决吗?我知道这会导致更多的约束解决工作,但正如存在不可判定的实例/不一致性一样,当“我知道我在做什么”时,难道不存在一致性实例上下文

我明白,当你

instance (Foo a) => Bar a
instance (Xyy a) => Bar a

GHC不考虑上下文,并且实例被报告为重复。

违反直觉的是,(我猜)在选择一个实例之后,它仍然需要检查上下文是否匹配,如果不匹配,则丢弃该实例。那么,为什么不颠倒顺序,放弃不匹配上下文的实例,继续处理剩余的集合呢


这会在某种程度上难以解决吗?我知道这会导致更多的约束解决工作,但正如存在
不可判定的实例
/
不一致性
一样,当“我知道我在做什么”时,难道不存在
一致性实例上下文
吗?

这并不能回答您为什么会出现这种情况的问题。但是,请注意,您始终可以定义一个新类型包装来消除两个实例之间的歧义:

newtype FooWrapper a = FooWrapper a
newtype XyyWrapper a = XyyWrapper a

instance (Foo a) => Bar (FooWrapper a)
instance (Xyy a) => Bar (XyyWrapper a)

这还有一个额外的优点,即通过传递FooWrapper或XyyWrapper,您可以显式地控制如果您的a恰好同时满足这两个条件,您希望使用哪一个实例。

类有点奇怪。最初的想法(仍然很有效)是一种围绕
数据
语句的语法糖。例如,你可以想象:

data Num a = Num {plus :: a -> a -> a, ... , fromInt :: Integer -> a}
numInteger :: Num Integer
numInteger = Num (+) ... id
然后,您可以编写具有以下类型的函数:

test :: Num x -> x -> x -> x -> x
test lib a b c = a + b * (abs (c + b))
    where (+) = plus lib
          (*) = times lib
          abs = absoluteValue lib
所以我们的想法是“我们将自动派生所有的库代码。”问题是,我们如何找到我们想要的库?如果我们有一个类型为
Num Int
的库,那么这很容易,但是我们如何基于以下类型的函数将其扩展到“受约束的实例”:

fooLib :: Foo x -> Bar x
xyyLib :: Xyy x -> Bar x
Haskell中当前的解决方案是对这些函数的输出类型进行类型模式匹配,并将输入传播到结果声明中。但当有两个相同类型的输出时,我们需要一个组合器将它们合并为:

eitherLib :: Either (Foo x) (Xyy x) -> Bar x
基本上,问题是目前还没有这样好的约束组合子。那是你的反对意见

好吧,这是真的,但在实践中也有一些方法可以实现类似的道德目标。假设我们定义了一些类型为的函数:

data F
data X
foobar'lib :: Foo x -> Bar' x F
xyybar'lib :: Xyy x -> Bar' x X
bar'barlib :: Bar' x y -> Bar x
显然,
y
是一种贯穿所有这些的“幻影类型”,但它仍然很强大,因为如果我们想要一个
Bar x
,我们将传播对
Bar'x y
的需求,如果需要
Bar'x y
我们将生成
Bar'x
Bar'x y
。因此,使用幻象类型和多参数类型类,我们可以得到我们想要的结果


更多信息:

在最坏的情况下,添加回溯将使实例解析需要指数时间

从本质上讲,实例成为形式的逻辑语句

P(x) => R(f(x)) /\ Q(x) => R(f(x))
((fastP(x) /\ P(x)) \/ (fastQ(x) /\ Q(x))) => R(f(x))
这相当于

(P(x) \/ Q(x)) => R(f(x))
根据计算,该检查的成本为(在最坏情况下)

假设
p
Q
的成本相似

c_R(n) = 2 * c_PQ(n-1)
这导致了指数增长

为了避免这个问题,重要的是要有快速的方法来选择分支,即有形式的子句

P(x) => R(f(x)) /\ Q(x) => R(f(x))
((fastP(x) /\ P(x)) \/ (fastQ(x) /\ Q(x))) => R(f(x))
其中,
fastP
fastQ
可在固定时间内计算,且不兼容,因此最多需要访问一个分支


Haskell认为这种“快速检查”是头部兼容性(因此忽略了上下文)。当然,它可以使用其他快速检查——这是一个设计决策。

这打破了开放世界的假设。假设:

class B1 a
class B2 a
class T a
如果我们允许约束消除实例的歧义,我们可以编写

instance B1 a => T a
instance B2 a => T a
instance B1 Int
可以写

instance B1 a => T a
instance B2 a => T a
instance B1 Int
现在,如果我有

f :: T a => a
然后
f::Int
工作。但是,开放世界的假设是,一旦某件事情起作用,增加更多的实例就无法打破它。我们的新系统不遵守:

instance B2 Int
将使
f::Int
不明确。应该使用
T
的哪个实现

另一种说法是你打破了连贯性。对于一致的类型类,意味着只有一种方法可以满足给定的约束。在普通Haskell中,约束
c
只有一个实现。即使有重叠的例子,连贯性通常也是正确的。其思想是,
instance ta
instance{-#OVERLAPPING#-}T Int
不会破坏连贯性,因为GHC不会被骗到在后者可以做到的地方使用前一个实例。(你可以骗孤儿,但你不应该。)连贯性,至少对我来说,似乎有点可取。从某种意义上说,类型类的用法是“隐藏的”,因此有必要强制要求它明确无误。你也可以用
不连贯性
和/或
不安全性
来破坏连贯性,但是,你知道


从范畴理论的角度来看,范畴
约束
很薄:从一个
约束
到另一个约束
最多有一个
实例
/箭头。我们首先构造两个箭头
a:()=>B1 Int
b:()=>B2 Int
,然后通过添加新的箭头
x_Int:B1 Int=>T Int
y_Int:B2 Int=>T Int
,从而打破稀疏性。a
y\u Int。b
是两个不相同的箭头
()=>T Int
。钻石问题,有人吗?

如果
a
Foo
Xyy
,GHC应该选择哪个实例?@mb14:任意。(
不连贯性
已经做了类似的事情,我可以接受)。事实上,唯一的区别似乎是
不连贯性
允许GHC提交任何一个实例,可能会丢弃具有可满足上下文的实例,并提交到不可满足的实例(这将触发错误)。相反,这个问题问如果我理解正确,为什么GHC没有
BacktrackOnContextFailures
标志,以便最终尝试正确的实例。在最坏的情况下,它肯定会导致难以解决的问题,但我们