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

时间:2014-11-12 10:47:14

标签: haskell typeclass

我明白了,

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

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

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

这会在某种程度上难以解决吗?我看到它如何能够提前做出更多的约束解析工作,但正如UndecidableInstances / IncoherentInstances一样,当{34}我不知道是什么时候会有ConsiderInstanceContexts我正在做"?

4 个答案:

答案 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 XBar' 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 ainstance {-# OVERLAPPING #-} T Int不会破坏连贯性,因为不能欺骗GHC在后者会使用的地方使用前者。 (您可以 与孤儿一起欺骗它,但您不应该这样做。)连贯性,至少对我来说,似乎有些可取。从某种意义上说,类型类的用法是“隐藏的”,并且有必要强制其明确。您也可以使用IncoherentInstances和/或unsafeCoerce破坏连贯性,但是,知道。

按照类别理论的观点,类别Constraint thin :从一个instance到另一个Constraint /箭头最多为一个。我们首先构造两个箭头a : () => B1 Intb : () => B2 Int,然后通过添加新箭头x_Int : B1 Int => T Inty_Int : B2 Int => T Int来打破稀疏性,使得x_Int . ay_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)

假设PQ具有相似的费用

c_R(n) = 2 * c_PQ(n-1)

这导致指数增长。

为避免此问题,重要的是要有一种快速的方法来选择一个分支,即具有以下形式的子句

((fastP(x) /\ P(x)) \/ (fastQ(x) /\ Q(x))) => R(f(x))

其中fastPfastQ可在恒定时间内计算,并且不兼容,因此最多需要访问一个分支。

Haskell认为此“快速检查”是头部兼容性(因此不考虑上下文)。当然,它可以使用其他快速检查-这是设计决定。