为什么扩展定义时我的功能依赖冲突会消失?

时间:2018-06-08 16:48:36

标签: haskell ghc functional-dependencies type-level-computation

我试图在Haskell中的类型级别实现Integers。首先,我用

实现了自然数字
data Zero
data Succ a

然后我用

将其扩展为整数
data NegSucc a

然后我决定创建一个增加整数的Increment类。我是这样做的:

{-# Language FunctionalDependencies #-}
{-# Language UndecidableInstances #-}
{-# Language MultiParamTypeClasses #-}
{-# Language FlexibleInstances #-}

import Prelude ()

-- Peano Naturals --

data Zero
data Succ a

class Peano a
instance Peano Zero
instance (Peano a) => Peano (Succ a)

-- Integers --

data NegSucc a -- `NegSucc a` is -(a+1) so that 0 can only be expressed one way

class Integer a
instance (Peano a) => Integer a
instance (Peano a) => Integer (NegSucc a)

-- Arithmetic Operations --

class Increment a b | a -> b
instance (Peano a) => Increment a (Succ a)
instance Increment (NegSucc Zero) Zero
instance (Peano a) => Increment (NegSucc (Succ a)) (NegSucc a)

然而,当我运行此GHC时,抱怨Functional dependencies conflict between instance declarations,并引用我的所有三个增量实例。这个错误对我来说没有多大意义,我没有看到单独声明之间的任何冲突。令我更加困惑的是,如果我将第一个实例更改为两个单独的实例

instance (Peano a) => Increment (Succ a) (Succ (Succ a))
instance Increment Zero (Succ Zero)

编译器完全停止抱怨。但是,定义Peano a的规则告诉我这两件事应该是相同的。

为什么在编写单行版本时会出现函数依赖冲突,但在编写双行版本时却没有?

2 个答案:

答案 0 :(得分:8)

这个答案是this comment的扩展,让我了解正在发生的事情

在Haskell类型中,类是一个开放类,这意味着可以在声明之后创建类的新实例。

这意味着我们无法推断NegSucc a不是Peano的成员,因为它总是可以在以后声明它。

instance Peano (NegSucc a)

因此当编译器看到

instance (Peano a) => Increment a (Succ a)
instance Increment (NegSucc Zero) Zero

它无法知道NegSucc Zero不会是Peano的实例。如果NegSucc ZeroPeano的实例,它将增加到ZeroSucc (NegSucc Zero),这是一个功能依赖冲突。所以我们必须抛出错误。这同样适用于

instance (Peano a) => Increment (NegSucc (Succ a)) (NegSucc a)

此处(NegSucc (Succ a))也可以是Peano的实例。

它看起来好像没有冲突的原因是我们隐含地假设没有Peano的其他实例而不是我们所知道的实例。当我将单个实例转换为两个新的意义时,我做出了正式的假设。

在新代码中

instance (Peano a) => Increment (Succ a) (Succ (Succ a))
instance Increment Zero (Succ Zero)

无法向预先存在的类型类添加任何内容,以使类型与多个冲突的实例匹配。

答案 1 :(得分:4)

这是我在原始版本中看到的错误消息:

negsucc.hs:28:10: error:
    Functional dependencies conflict between instance declarations:
      instance Peano a => Increment a (Succ a)
        -- Defined at negsucc.hs:28:10
      instance Increment (NegSucc Zero) Zero
        -- Defined at negsucc.hs:29:10
      instance Peano a => Increment (NegSucc (Succ a)) (NegSucc a)
        -- Defined at negsucc.hs:30:10

我对此的理解是:

  • 功能依赖a -> b意味着GHC将"模式匹配"在a类型上,我们会根据b发现a
  • GHC 在此"模式匹配"中包含超类约束逻辑。这个很重要。直觉上,你知道NegSucc不是Peano的一个例子。但是,GHC不知道如何查看NegSucc并排除需要Peano NegSucc的实例。 (这只是了解GHC实例选择的工作原理。可以想象,GHC未来的改进可能会在这种情况下有所改善。)
  • 因此,
  • instance Peano a => Increment a (Succ a)将始终由GHC选择,因为它是全能Increment a。任何其他已定义的实例都将与此实例冲突。

现在,UndecidableInstances的用途是什么?允许冲突的实例,并允许GHC选择适用的最具体的实例?你可能这么认为。我是这么想的。但请注意,错误消息是专门讨论Functional dependencies conflict,这意味着我并不认为UndecidableInstances能够以这种方式充分实现处理重叠的FunDeps。

-- pseudocode for the fundep a -> b
increment a = Succ a
increment (NegSucc Zero) = Zero
increment (NegSucc (Succ a)) = NegSucc a
-- note that this is just pseudocode; unlike Haskell,
-- instead of trying cases from top to bottom
-- all cases will be tried simultaneously

但是,如果你"扩展&#34>,这个问题就不存在了。正如你所做的那样定义。

-- pseudocode for the fundep a -> b with expanded defs
increment Zero = Succ Zero
increment (Succ a) = Succ (Succ a)
increment (NegSucc Zero) = Zero
increment (NegSucc (Succ a)) = NegSucc a
-- notice how there is now no overlap on the LHS pattern matches

您也可以通过使用原始defs翻转fundep来解决问题。

-- pseudocode for the fundep b -> a with original defs
-- I called it "decrement" instead,
-- because from the b -> a point of view, that is what it does
decrement (Succ a) = a
decrement Zero = NegSucc Zero
decrement (NegSucc a) = NegSucc (Succ a)
-- notice how there is no overlap on the LHS pattern matches

我在这里伸展自己对fundeps的了解,如果我错了,请有人纠正我。