添加约束会导致其他约束超出范围吗?

Can adding a constraint cause other constraints to go out of scope?

考虑以下代码,它进行类型检查:

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 做一些额外的工作,这取决于以下相当复杂的实例 class:

-- 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) 约束。此外,我们还没有删除或修改编译器用来在 foo 中详细说明 Eq (t a a) 实例的约束;除了新约束之外,我们仍然需要 (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) =>
                           ()

鉴于编译器可以“从上下文 (Eq2 t, Eq a)”“推断出 Eq (t a a)”,我不明白为什么更丰富的上下文 (Eq2 t, Eq a, SomeClass t) 会导致 Eq (t a a) 变得不可用。

这是一个错误,还是我做错了什么?在这两种情况下,是否有一些解决方法?

编辑:这个变通办法被证明是一个不可行的变通办法,因为它不能在 GHC 8.8.3 中编译。奇怪的是,analogous real world program 在 GHC 8.6.5 中编译得很好,甚至通过了一系列测试,尽管编译成功是一个错误的结果。


我找到了一种解决方法,涉及“分解”我所依赖的附加 class 中的量化。因此,如果我进行以下更改:

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' 中量化变量的方式相关(在这种情况下,它并不是一个真正可行的解决方案)。

此外,在我看来,foo' 上的附加 SomeClass 约束(无论 SomeClass 是如何定义的)会以某种方式导致编译器忘记,这是一个错误如何从可用的 Eq2 tEq a 构建一个 Eq (t a a)。这个直觉是不是不对?

欢迎补充说明这两点的答案。

我认为您 运行 违反了重叠公理的 "Reject if in doubt" 规则。当您将 SomeClass t 约束引入范围时,您还引入了新的量化约束 forall ob x y. (ob x, ob y) => ob (t x y)。到了释放Eq (t a a)的时候,GHC不知道是用foo签名中的量化约束Eq2 t还是SomeClass[=中的量化约束33=],因为任何一个都适用。 (与往常一样,GHC 在评估多态实例是否适用时不考虑 SomeConstraint t ~ ob。)没有机制来检查后者是否可以“专门化”到前者。

如果从 foo 中删除 Eq2 t 约束:

foo :: forall u t a.
  ( SomeClass t
  , Eq a
  ) => ()
foo = ensure @(Eq (a `t` a)) ()

然后你会得到一个错误“Couldn't match type SomeConstraint t with Eq”,暗示这正是 GHC 试图解决这个约束的方式。 (如果您从 class 中删除 SomeConstraint t ~ ob,它甚至会进行类型检查!)

这不能解决您的问题,但我认为它可以解释发生了什么。

这不是真正的错误; it's expected。在foo的定义中,上下文有

  1. forall x y. (Eq x, Eq y) => Eq (t x y)(即Eq2 t
  2. Eq a
  3. SomeClass t
  4. 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) 仍然是 available,但它被认为是 after 上下文中的所有“真实”超类(因为它不是实际上不是任何东西的超类)。

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 目前比较保守,出于性能、简单性和可预测性的原因,只是放弃而不是回溯。