这两种类型级Peano自然数求和的定义之间是否存在实际差异?

时间:2018-02-14 16:10:01

标签: haskell types typechecking dependent-type

我是类型级编程的新手,我无法推理甚至非常简单的类型级程序。

我有这段代码:

data Nat = Zero | Succ Nat

type family n + m where
    Zero + m = m  -- This is the base case of my type level recursive function.

现在,我有两种关于递归案例的变体,其中只有一种没有UndecidableInstances的类型检查:

    Succ n + m = n + Succ m  -- Typechecks with UndecidableInstances.

    Succ n + m = Succ (n + m)  -- Typechecks without UndecidableInstances.

我理解UndecidableInstances允许我在类型级别上使用一般递归,而不是单独使用结构递归,这将保证类型检查器终止。但实际上,这两个定义似乎编译得同样好,并且在使用repl中的:kind!进行验证时,对于较小的数字,可以给出理智的类型。

我可以确定这两个定义在所有方面都是等价的,还是存在极端情况?我应该相互测试+的这些实现,还是使用某种理论?

2 个答案:

答案 0 :(得分:2)

假设

foo :: T (Succ n) -> Bool
bar :: T n -> T m -> T (n + m)

然后,

baz :: T (Succ a) -> String
baz x | foo (bar x x) = "A"
      | otherwise     = "B"

只会输入检查是否使用

定义了+
Succ n + m = Succ (n + m)

如果我们使用

Succ n + m = n + Succ m

然后,在baz的类型检查期间,我们会发现bar x x :: a + Succ (Succ a),但这不是foo的参数的正确形式。

总结:仅考虑“groundNat类型是不够的。在类型检查期间,我们还遇到具有自由类型变量(即非基础)的类型表达式,如Succ a + Succ a,这将根据等式规则进行简化。我们也需要适用于此类案例的规则。

答案 1 :(得分:2)

正如其他人所指出的那样,这两个定义并不相同。两者都没有比另一个更普遍,并且认为一方或另一方明显优越是一种延伸。

正如@Benjamin Hodgson指出的那样,定义的选择会影响编写代码的难易程度,但我补充一点,通常每个选项都会使代码更容易,其他代码更难。

这是一个具体的例子。假设您有一个矢量类型:

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

data Nat = Zero | Succ Nat

-- A vector type
data Vec n a where
  V0 :: Vec Zero a
  (:>) :: a -> Vec n a -> Vec (Succ n) a
infixr 5 :>

让我们从使用(+)的“漂亮”版本开始,这个版本不需要不可判定的实例。请注意,我已切换mn的顺序,因为我感到困惑:

type family m + n where
  Zero + m = m
  Succ m + n = Succ (m + n)

如果我们尝试为矢量实现(++),我们会发现它很简单:

vappend :: Vec m a -> Vec n a -> Vec (m + n) a
vappend V0 ys = ys
vappend (x :> xs) ys = x :> vappend xs ys

另一方面,请考虑以下列表功能:

rev :: [a] -> [a] -> [a]
rev ys [] = ys
rev ys (x:xs) = rev (x:ys) xs

可能用于定义reverse的版本:

reverse' = rev []

如果我们尝试实施vrev,我们会遇到麻烦:

vrev :: Vec m a -> Vec n a -> Vec (n + m) a
vrev ys V0 = ys
vrev ys (x :> xs) = vrev (x :> ys) xs

第一个案例类型检查没问题,但第二个案件因GHC无法推断的投诉而失败:

(n1 + 'Succ m) ~ 'Succ (n1 + m)

尽管这句话“显然”是真的。

在签名中用n + m替换m + n会让事情变得更糟 - 两种情况都不会进行类型检查。

另一方面,如果用不可判定的版本替换+定义:

Succ m + n = m + Succ n

你会发现vrev类型检查正常而vappend没有!

那么,解决方案是什么?好吧,通常人们会选择在大多数情况下看起来最方便的定义,然后使用各种技术来处理“硬”案例。如果您选择:

Succ m + n = Succ (m + n)

定义,你会成为一个好公司。例如,Hasochism papersingletons-nats包中使用的那个。