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

时间:2020-07-20 08:40:42

标签: haskell typeclass quantified-constraints

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

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)变得不可用。

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

3 个答案:

答案 0 :(得分:5)

这并不是一个错误。 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)仍然可用,但是在上下文中所有“ 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 tEq匹配”,这表明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。这种直觉是不正确的吗?

欢迎您提供这两点的补充答案。