考虑以下代码,进行类型检查:
module Scratch where
import GHC.Exts
ensure :: forall c x. c => x -> x
ensure = id
type Eq2 t = (forall x y. (Eq x, Eq y) => Eq (t x y) :: Constraint)
foo :: forall t a.
( Eq2 t
, Eq a
) => ()
foo = ensure @(Eq (a `t` a)) ()
foo
在这里没有做任何有用的事情,但让我们想象一下它正在做一些重要的业务,这些业务需要一个Eq (t a a)
实例。编译器能够采用(Eq2 t, Eq a)
约束并详细说明Eq (t a a)
字典,因此约束就解除了,一切正常。
现在假设我们希望foo
做一些额外的工作,这些工作取决于以下相当复杂的类的实例:
-- some class
class (forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => ob (t x y)) =>
SomeClass t
where
type SomeConstraint t :: * -> Constraint
foo' :: forall t a.
( Eq2 t
, Eq a
, SomeClass t -- <- the extra constraint
) => ()
foo' = ensure @(Eq (a `t` a)) ()
请注意,在foo'
的正文中,我们仍然只要求我们在foo
中所做的事情:Eq (t a a)
约束。此外,我们没有删除或修改编译器用来详细说明Eq (t a a)
中的foo
实例的约束;除了新的约束条件外,我们仍然需要(Eq2 t, Eq a)
。因此,我希望foo'
也能进行类型检查。
不幸的是,看起来实际发生的情况是编译器忘记了如何详细说明Eq (t a a)
。这是我们在foo'
正文中得到的错误:
• Could not deduce (Eq (t a a)) arising from a use of ‘ensure’
from the context: (Eq2 t, Eq a, SomeClass t)
bound by the type signature for:
foo' :: forall (t :: * -> * -> *) a.
(Eq2 t, Eq a, SomeClass t) =>
()
鉴于编译器可以“从上下文Eq (t a a)
”中推断出“ (Eq2 t, Eq a)
”,我不明白为什么更丰富的上下文(Eq2 t, Eq a, SomeClass t)
导致Eq (t a a)
变得不可用。
这是一个错误,还是我做错了什么?在这两种情况下,都有解决方法吗?
答案 0 :(得分:5)
这并不是一个错误。 it's expected。在foo
的定义中,上下文具有
forall x y. (Eq x, Eq y) => Eq (t x y)
(即Eq2 t
)Eq a
SomeClass t
forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => ob (t x y)
(来自“ {上的超类关系的关闭” SomeClass t
)我们想要Eq (t a a)
。嗯,从上下文来看,有两个公理的头部匹配:(1)与x ~ a, y ~ a
和(2)与ob ~ Eq, x ~ a, y ~ a
。有疑问; GHC拒绝。 (请注意,由于SomeConstraint t ~ ob
仅存在于(4)的假设中,因此它被完全忽略了;选择实例只关注实例头。)
前进的明显方法是从SomeClass
的超类中删除(4)。怎么样?从实际实例“ head”拆分量化:
class ob (t x y) => SomeClassSuper ob t x y where
instance ob (t x y) => SomeClassSuper ob t x y where
class (forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => SomeClassSuper ob t x y) => SomeClass t where
type SomeConstraint t :: * -> Constraint
这是您的forall ob. _ => forall x y. _ => _
技巧的基本操作,除了这不依赖于错误(不允许使用语法)。现在,(4)变为forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => SomeClassSuper ob t x y
。因为这实际上不是Class args...
形式的约束,所以它没有超类,因此GHC不会向上搜索并找到破坏一切的强大forall ob x y. ob (t x y)
头。现在唯一可以释放Eq (t a a)
的实例是(1),所以我们使用它。
GHC 确实在绝对需要时搜索新(4)的“超类”;实际上,《用户指南》将此功能扩展为上述基本规则的延伸,这些规则来自原始论文。也就是说,forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => ob (t x y)
仍然可用,但是在上下文中所有“ true”超类都被认为是“ 之后”(因为它实际上不是的超类)。任何东西)。
import Data.Kind
ensure :: forall c x. c => ()
ensure = ()
type Eq2 t = (forall x y. (Eq x, Eq y) => Eq (t x y) :: Constraint)
-- fine
foo :: forall t a. (Eq2 t, Eq a) => ()
foo = ensure @(Eq (t a a))
class ob (t x y) => SomeClassSuper ob t x y where
instance ob (t x y) => SomeClassSuper ob t x y where
class (forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => SomeClassSuper ob t x y) => SomeClass t where
type SomeConstraint t :: * -> Constraint
-- also fine
bar :: forall t a. (Eq2 t, Eq a, SomeClass t) => ()
bar = ensure @(Eq (t a a))
-- also also fine
qux :: forall t a. (Eq2 t, Eq a, SomeConstraint t a, SomeClass t) => ()
qux = ensure @(SomeConstraint t (t a a))
您可能会争辩说,根据开放世界政策,GHC 应该在“不连贯”的情况下倒退(例如(1)与原始(4)之间的重叠),因为量化的约束可以制造“不连贯”,而没有实际的不连贯,因此我们希望您的代码“可以正常工作”。这是一个完全正确的需求,但是GHC目前是保守的,出于性能,简单性和可预测性的原因,它只是放弃而不是回溯。
答案 1 :(得分:2)
我认为您违反了"Reject if in doubt"规则,即公理重叠。将SomeClass t
约束纳入范围时,还将引入新的量化约束forall ob x y. (ob x, ob y) => ob (t x y)
。当需要释放Eq (t a a)
时,GHC不知道是使用Eq2 t
签名中的量化约束foo
还是SomeClass
类中的量化约束,因为任何一个都适用。 (与往常一样,GHC在评估多态实例是否适用时不会考虑SomeConstraint t ~ ob
。)没有机制可以检查后者可以“专门化”为前者。
如果您从Eq2 t
删除foo
约束:
foo :: forall u t a.
( SomeClass t
, Eq a
) => ()
foo = ensure @(Eq (a `t` a)) ()
然后您将收到错误消息“无法将类型SomeConstraint t
与Eq
匹配”,这表明GHC正试图解决此约束。 (如果从SomeConstraint t ~ ob
中删除class
,它甚至会进行类型检查!)
这不能解决您的问题,但是我认为这可以说明正在发生的事情。
答案 2 :(得分:0)
编辑:该替代方法实际上是一种不可行方法,因为它无法在GHC 8.8.3中进行编译。神秘地,analogous real world program在GHC 8.6.5中可以很好地进行编译,甚至可以通过一系列测试,尽管编译的成功是错误的结果。
我找到了一种解决方法,其中涉及“分解”我所依赖的其他类中的量化。因此,如果我进行以下更改:
class
-- (forall ob x y. (SomeConstraint t ~ ob, ob x, ob y) => ob (t x y)) =>
(forall ob. SomeConstraint t ~ ob => forall x y. (ob x, ob y) => ob (t x y)) =>
SomeClass t
where
type SomeConstraint t :: * -> Constraint
它进行foo'
类型检查。
我仍然不太了解为什么这会使事情起作用,以及它是否以某种方式与我对foo'
中的变量进行量化的方式耦合在一起(在这种情况下,这实际上不是可行的解决方案)。 >
此外,在我看来,这仍然是一个错误,即对SomeClass
的附加foo'
约束(无论SomeClass
的定义如何)都会导致编译器忘记如何从可用的Eq (t a a)
和Eq2 t
中构建一个Eq a
。这种直觉是不正确的吗?
欢迎您提供这两点的补充答案。