GADT类型的奇怪类型推断行为(对于固定长度向量)

时间:2012-08-22 14:14:05

标签: haskell ghc typeclass gadt

我有以下GADT

data Vec n a where
    T    :: Vec VZero a
    (:.) :: (VNat n) => a -> (Vec n a) -> (Vec (VSucc n) a)

使用类

对固定长度向量进行建模
class VNat n

data VZero
instance VNat VZero

data VSucc n
instance (VNat n) => VNat (VSucc n)

我尝试在vector上编写一个append-function:

vAppend :: Vec n b -> Vec m b -> Vec nm b
vAppend T T = T   -- nonsense, -- its just a minimal def for testing purposes

类型检查器不喜欢它:

 Could not deduce (nm ~ VZero)
    from the context (n ~ VZero)
      bound by a pattern with constructor
                 T :: forall a. Vec VZero a,
               in an equation for `vAppend'
      at VArrow.hs:20:9
    or from (m ~ VZero)
      bound by a pattern with constructor
                 T :: forall a. Vec VZero a,
               in an equation for `vAppend'
      at VArrow.hs:20:11
      `nm' is a rigid type variable bound by
           the type signature for vAppend :: Vec n b -> Vec m b -> Vec nm b
           at VArrow.hs:20:1
    Expected type: Vec nm b
      Actual type: Vec VZero b
    In the expression: T
    In an equation for `vAppend': vAppend T T = T
Failed, modules loaded: Vectors.

任何人都可以用类型变量nm解释GHC问题吗?究竟什么意思是错误消息中的~

3 个答案:

答案 0 :(得分:10)

就目前而言,你说你可以通过附加任何两个向量来获得任何长度的向量。如果你废弃签名ghc推断vAppend应该在任意两个向量的情况下产生长度为VZero的向量 - 这是有道理的,但不是你想要的。您需要与Plus关联的VNat类型来约束向量上vAppend的结果类型。自然的方式是某种类型的家庭,但我无法在VNat类下得到它。在任何情况下,使用DataKinds扩展的推广类型(在ghc-7.4及更高版本中)可以更清楚地实现这一整个想法 - 尽管您可能是故意避免扩展?这消除了VSucc的令人讨厌的未闭合字符,它承认VSucc Char等。如果您不想避免DataKinds,那么您的模块可能看起来像这样:

{-#LANGUAGE  GADTs, TypeFamilies, DataKinds, TypeOperators#-}

data Vec n a where                 -- or: data Vec :: VNat -> * -> * where
    T    :: Vec VZero a
    (:.) :: a -> Vec n a -> Vec (VSucc n) a 
                                   -- no class constraint

data VNat  =  VZero |  VSucc VNat  -- standard naturals

type family n :+ m :: VNat         -- note the kind of a ":+" is a promoted VNat
type instance VZero :+ n = n
type instance VSucc n :+ m = VSucc (n :+ m)

vAppend :: Vec n b -> Vec m b -> Vec (n :+ m) b
vAppend T v = v  
vAppend (a :. u) v  = a :. (vAppend u v)

编辑:我看到这一点,Plus - 或:+ - 类型系列的行应该明确约束参数的类型

type family (n::VNat) :+ (m::VNat) :: VNat

保留像Char :+ VZero之类的垃圾类型等等 - 也就是说,使用与DataKinds之类的data VSucc n类型相同的原则。此外,我们可以看到两个实例完全指定它,但我不知道编译器可以使用多少这个。

答案 1 :(得分:9)

类型签名中的所有类型变量都是普遍量化的。这意味着,如果您说该函数具有类型

Vec n b -> Vec m b -> Vec nm b

然后,对于nmnmb的任何选择,此类型必须有效。特别是,例如,

Vec VZero Int -> Vec VZero Int -> Vec (VSucc VZero) Int

也必须是您的函数的有效类型。但是,附加两个向量不具有这种通用类型。 nm存在约束,即nm是类型级数nm的总和。您必须在函数类型中表达这些约束,否则您将获得类型错误。

在您的情况下,GHC抱怨说,在您的定义中,nm实际上是VZero,因此您假设您的类型表明您不允许nm ~。 {{1}}只是GHC的类型相等符号。

答案 2 :(得分:6)

当通过模式匹配在GADT的值上编写函数时,GHC在类型检查每个子句时使用有关函数运行时的预期行为的信息。您的vAppend函数只有一个子句,该模式匹配类型Vec n b的值和另一个类型Vec m b的值。 GHC的原因是,如果在运行时vAppend应用于与模式T匹配的实际参数,那么实际参数的实际类型必须是Vec VZero b形式,这是一个比Vec n bVec m b更具信息性的类型。在GHC中实现这种推理的方式是,当类型检查vAppend的唯一子句的RHS时,它会记录n必须确实是VZero的假设,写{{1}同样n ~ VZero

您为函数编写的类型列出了必须履行的合同。您获得的错误消息是因为m ~ VZero的实现必须履行的合同过于笼统。你是说给定两个长度为vAppendn的向量,m必须产生一个可以被认为是任何大小的向量。实际上,GHC注意到您的实施不符合此合同,因为您的RHS类型vAppend与RHS的预期类型Vec VZero b不匹配,并且没有假设{ {1}}。实际上,GHC告诉我们,唯一可用的假设是前一段中的假设。

您履行此合同的唯一可能方法是将Vec nm b写为您的条款的RHS。这显然不是你想要的。为nm ~ VZero获取正确类型的技巧是以某种方式将输出向量的大小与两个输入向量的相应大小相关联。这可能是这样的:

undefined

我们在这里所做的是说,长度由vAppend的输入长度决定,通过一些名为type family Plus n m type instance Plus VZero m = m type instance Plus (VSucc n) m = VSucc (Plus n m) vAppend :: Vec n b -> Vec m b -> Vec (Plus n m) b vAppend T T = T 的类型的函数来确定。在两个输入长度均为vAppend的情况下,我们知道PlusVZeroPlus n m之后的Plus VZero VZero相同。由于n ~ VZero具有第一类型族实例的形状,因此GHC知道它与m ~ VZero相同。因此,在这个分支中,GHC期望Plus VZero VZero类型的RHS,我们可以很容易地构建它!