前置知识
自然数
自然数类型是 Nat,有两种构造器 zero 和 add1,其中 add1 接受一个 Nat 参数。因此 0 就是 zero,1 就是 (add1 zero),2 就是 (add1 (add1 zero)),等等。Nat 有一个归纳析构器 ind-Nat,这样就可以不用递归写 Nat 的依赖函数啦。
相等类型
Pie 的内置类型 (= E e1 e2) 表达“类型为 E 的 e1 和 e2 相等” 这一命题,唯一的构造器(也就是证明)是 (same e),产生一个类型为 (= E e e) 的值;当 e1 和 e2 不相等时,没有 (= E e1 e2) 类型的值。
反命题、归缪
直觉主义逻辑表达假命题的方式是“推出谬”,在 Pie 中就是 (→ X Absurd) 类型,Absurd 类型没有值,却有一个可以导出任意类型的析构器(eliminator)ind-Absurd,因此命题 a ≠ b 对应的类型是 (→ (= E a b) Abusrd)。
0≠1
在 Agda 里以上都可以自定义,0 ≠ 1 这种命题也可以用谬模式(absurd pattern)轻易地写出来:
1 | data ≡ {A : Set} (x : A) : A → Set where |
最后一个 () 就是谬模式,意思就是编译器和你都知道那玩意是根本构造不出来的,所以以谬推谬,返回啥都行。
但 Pie 可没有这么聪明,它能做到的事更多是“检查”而非“推导”。看起来想写出 (→ (= zero (add1 zero)) Absurd) 很困难,怎样才能从一个不存在的东西((= zero (add1 zero)))得到另一个不存在的东西(Absurd)呢?
我们可以用依赖类型(dependent type)的精髓——从值到类型来解决这个问题。首先定义 =consequence,它把两个自然数映射到一个类型上,其中 zero 和 zero 结果是 Trival,它们显然相等;zero 和 (add1 n) 或者 (add1 n) 和 zero 结果是 Absurd,它们显然不相等;(add1 m) 和 (add1 n) 的结果则是 (= Nat m n)——它们的相等性可由 m 和 n 的相等性归纳给出。=consequence 的完整定义:
1 | (claim =consequence (→ Nat Nat U)) |
《The Little Typer》最后两章可以说以 =consequence 为基础。=consequence 是类型生成器,我们还需要构造对应的值:
1 | (claim =consequence-same |
=consequence-same 很简单,同一个数和自己肯定是相等的,分类讨论 zero 和 add1 即可;有意思的是 use-Nat=,它把一个 (= Nat m n),也就是 m 等于 n 的证明映射为一个 (=consequence m n)!其思路比 =consequence-same 还简单,用 replace 把 (=consequence m n) 转换为 (=consequence m m),再用 (=consequence-same m) 证明之。
注意到了吗,如果 m 和 n 并不相等,此时 (=consequence m n) 就是 Absurd,我们成功完成了“从无到无”的构造!而 use-Nat= 内部并没有什么矛盾——从类型上看,传入的 (= Nat m n) 就意味着 m 与 n 相等。
事实上我们可以把 =consequence 定义中的 Absurd 换成其它类型,比如 Atom,则 Atom 就会成为 use-Nat 在 m 与 n 不等时返回的类型,而实际上我们并没有构造任何一个 Atom,此时就有了 ind-Absurd 的类似物,可以看出 Absurd 类型在系统中的作用可以由 (= Nat 0 1) 或者 (= Nat 1 2) 及无数个其它表达不成立相等命题的类型来取代。
于是我们就能愉快地证明 0 ≠ 1 啦:
1 | (claim 0≠1 (→ (= Nat zero (add1 zero)) Absurd)) |