我明白了,
instance (Foo a) => Bar a
instance (Xyy a) => Bar a
GHC不考虑上下文,并且实例报告为重复。
什么是违反直觉的,(我猜)在选择实例后,仍然需要检查上下文是否匹配,如果不匹配,则丢弃该实例。那么为什么不颠倒顺序,丢弃具有不匹配上下文的实例,然后继续使用剩余的集合。
这会在某种程度上难以解决吗?我看到它如何能够提前做出更多的约束解析工作,但正如UndecidableInstances
/ IncoherentInstances
一样,当{34}我不知道是什么时候会有ConsiderInstanceContexts
我正在做"?
答案 0 :(得分:3)
这不能回答你为什么会这样的问题。但请注意,您始终可以定义新类型包装器以消除两个实例之间的歧义:
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恰好满足两者。
答案 1 :(得分:2)
课程有点奇怪。最初的想法(它仍然非常有用)是围绕原本data
陈述的一种语法糖。例如,您可以想象:
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 X
或Bar' x y
。因此,对于幻像类型和多参数类型类,我们得到了我们想要的结果。
更多信息:https://www.haskell.org/haskellwiki/GHC/AdvancedOverlap
答案 2 :(得分:2)
这打破了开放世界的假设。假设:
class B1 a
class B2 a
class T a
如果我们允许约束消除歧义,我们可以写
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 T a
和instance {-# OVERLAPPING #-} T Int
不会破坏连贯性,因为不能欺骗GHC在后者会使用的地方使用前者。 (您可以 与孤儿一起欺骗它,但您不应该这样做。)连贯性,至少对我来说,似乎有些可取。从某种意义上说,类型类的用法是“隐藏的”,并且有必要强制其明确。您也可以使用IncoherentInstances
和/或unsafeCoerce
破坏连贯性,但是,知道。
按照类别理论的观点,类别Constraint
是 thin :从一个instance
到另一个Constraint
/箭头最多为一个。我们首先构造两个箭头a : () => B1 Int
和b : () => B2 Int
,然后通过添加新箭头x_Int : B1 Int => T Int
,y_Int : B2 Int => T Int
来打破稀疏性,使得x_Int . a
和y_Int . b
两个箭头() => T Int
都不相同。钻石问题,有人吗?
答案 3 :(得分:1)
在最坏的情况下,添加回溯将使实例解析需要指数时间。
从本质上讲,实例成为以下形式的逻辑语句
P(x) => R(f(x)) /\ Q(x) => R(f(x))
等效于
(P(x) \/ Q(x)) => R(f(x))
通过计算,此检查的费用为(在最坏的情况下)
c_R(n) = c_P(n-1) + c_Q(n-1)
假设P
和Q
具有相似的费用
c_R(n) = 2 * c_PQ(n-1)
这导致指数增长。
为避免此问题,重要的是要有一种快速的方法来选择一个分支,即具有以下形式的子句
((fastP(x) /\ P(x)) \/ (fastQ(x) /\ Q(x))) => R(f(x))
其中fastP
和fastQ
可在恒定时间内计算,并且不兼容,因此最多需要访问一个分支。
Haskell认为此“快速检查”是头部兼容性(因此不考虑上下文)。当然,它可以使用其他快速检查-这是设计决定。