h5i
本中文版由作者借助 AI 翻译而成。作者并非中文母语者,译文未经人工逐句校对,可能存在错误。如发现问题,请在 GitHub 上提交 issue。

第 6 章

逐步示例

本章在几个小项上手工运行算法 5.1,并把每一次运行与类型规则对应起来。所选的例子覆盖了算法的各项工作:构造推导、抓出错误的证明、使用假设、实例化全称命题,以及在类型内部进行计算。下面的每一条追踪都可以在演练场中用内核复现。

要验证 tt 是定理 TT 的证明,内核调用 Check(⋅,t,T)\Check(\cdot, t, T);它调用 Infer(⋅,t)\Infer(\cdot, t),再把结果与 TT 作比较。追踪展示了 Infer\Infer 发出的递归调用,以及每次调用所对应的类型规则。展开或折叠各节点即可跟随递归过程。

恒等证明

令 Γ1=P:Sort 0\Gamma_1 = P : \Sort{0},Γ2=Γ1,h:P\Gamma_2 = \Gamma_1, h : P。第 1 章开篇例子的推导,略去良构性前提后,是

⋅⊢Sort 0:Sort 1;(T-Sort)Γ1⊢P:Sort 0;(T-Var)Γ2⊢h:P;(T-Var)Γ1⊢λ(h:P). h:P→P;(T-Lam)⋅⊢λ(P:Sort 0). λ(h:P). h:Π(P:Sort 0). P→P;(T-Lam)\dfrac{ \dfrac{}{\cdot \vdash \Sort{0} : \Sort{1}}\rulename{T-Sort} \qquad \dfrac{ \dfrac{}{\Gamma_1 \vdash P : \Sort{0}}\rulename{T-Var} \qquad \dfrac{}{\Gamma_2 \vdash h : P}\rulename{T-Var} }{\Gamma_1 \vdash \tLam{h}{P}{h} : P \to P}\rulename{T-Lam} }{\cdot \vdash \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}} : \tPi{P}{\Sort{0}}{P \to P}}\rulename{T-Lam}

算法自下而上构造出同一棵推导。调用 Infer(⋅,λ(P:Sort 0). λ(h:P). h)\Infer(\cdot, \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}}) 的过程如下。

  • Infer(⋅,  λ(P:Sort 0). λ(h:P). h)\Infer(\cdot,\; \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}}) T-Lam
    • InferSort(⋅,  Sort 0)\InferSort(\cdot,\; \Sort{0}):Infer\Infer 返回 Sort 1\Sort{1},所以标注是一个类型 T-Sort
    • Freshen\Freshen 保持 PP 不变,因为上下文为空
    • Infer(Γ1,  λ(h:P). h)\Infer(\Gamma_1,\; \tLam{h}{P}{h}) T-Lam
      • InferSort(Γ1,  P)\InferSort(\Gamma_1,\; P):Infer\Infer 查找 PP 并返回 Sort 0\Sort{0} T-Var
      • Freshen\Freshen 保持 hh 不变
      • Infer(Γ2,  h)\Infer(\Gamma_2,\; h) 查找 hh 并返回 PP T-Var
      • 返回 Π(h:P). P\tPi{h}{P}{P}
    • 返回 Π(P:Sort 0). Π(h:P). P\tPi{P}{\Sort{0}}{\tPi{h}{P}{P}}

每一次返回类型的调用都对应推导树的一个节点,而且没有任何调用用到计算。最后,Check\Check 把结果与所声明的类型 Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} 作比较。两者只差一个约束变量的名字,因为 P→PP \to P 是 Π(_:P). P\tPi{\_}{P}{P} 的缩写,而 hh 和 _\_ 都不出现在体中。于是 IsDefEq\IsDefEq 成功,证明被接受。

下面是由内核自己记录的同一次运行:调用视图与上面的列表一致,证明树视图与推导一致。

# 同一个证明,以完整的项给出。内核推断它的类型,
# 再与目标作比较。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => h
qed

一个错误的证明

假设用户声称 t=λ(P:Sort 0). λ(h:P). Pt = \tLam{P}{\Sort{0}}{\tLam{h}{P}{P}} 证明了 Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P}。它的体返回的是命题 PP,而不是证明 hh。内核调用 Check(⋅,t,Π(P:Sort 0). P→P)\Check(\cdot, t, \tPi{P}{\Sort{0}}{P \to P}),后者先调用 Infer\Infer,各次调用与上面相同,只是最后一次 Infer(Γ2,P)\Infer(\Gamma_2, P) 返回 Sort 0\Sort{0}。于是 Infer\Infer 成功,结果为

Infer(⋅,t)  =  Π(P:Sort 0). Π(h:P). Sort 0.\Infer(\cdot, t) \;=\; \tPi{P}{\Sort{0}}{\tPi{h}{P}{\Sort{0}}}.

项 tt 是良类型的,却不能证明所声称的命题。Check\Check 比较这两个类型时,发现两边都无法继续归约,重命名约束变量也不能使它们相等:最内层的体一边是 Sort 0\Sort{0},另一边是 PP。因此 IsDefEq\IsDefEq 失败,Check\Check 报告类型不匹配。(第 11 章用 IsDefEq\IsDefEq 的算法逐步重演这次比较。)

这个错误之所以能被发现,只是因为用户声明了要证明的命题,而一个只运行 Infer\Infer 的内核会接受 tt,并悄悄给它分配一个不同的类型。这就是为什么每个脚本都以 goal 开头,也是为什么 qed 要对照它来检查。

goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => P
qed

使用假设:肯定前件

到目前为止的例子从未应用过函数。考虑检查

t=λ(P:Sort 0). λ(Q:Sort 0). λ(f:P→Q). λ(p:P). f  pagainstΠ(P:Sort 0). Π(Q:Sort 0). (P→Q)→P→Q,t = \tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{f}{P \to Q}{\tLam{p}{P}{f\;p}}}} \quad\text{against}\quad \tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{(P \to Q) \to P \to Q}},

它说的是“如果 PP 蕴涵 QQ 且 PP 成立,那么 QQ 成立”。四个 λ\lambda 像之前一样把上下文扩展为 Γ=P:Sort 0,  Q:Sort 0,  f:P→Q,  p:P\Gamma = P : \Sort{0},\; Q : \Sort{0},\; f : P \to Q,\; p : P。体 f  pf\;p 进入 Infer\Infer 的应用分支:

  • Infer(Γ,  f  p)\Infer(\Gamma,\; f\;p) T-App
    • Infer(Γ,  f)\Infer(\Gamma,\; f) 返回 P→QP \to Q,即 Π(_:P). Q\tPi{\_}{P}{Q}。Whnf\Whnf 保持它不变,它是一个 Π\Pi T-Var
    • Check(Γ,  p,  P)\Check(\Gamma,\; p,\; P) 成功,因为 p:Pp : P 在 Γ\Gamma 中 T-Var, T-Conv
    • 返回 Q[_↦p]=Q\subst{Q}{\_}{p} = Q。名字 _\_ 不出现在 QQ 中,所以替换什么也不做

如果用户写的是 f  ff\;f,第二步就会调用 Check(Γ,f,P)\Check(\Gamma, f, P),它会失败,因为 P→QP \to Q 不是 PP:在这里函数不能喂给它自己。试一试:把下面的 f p 改成 f f。

goal forall (P : Sort 0), forall (Q : Sort 0), (P -> Q) -> P -> Q
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (f : P -> Q) => fun (p : P) => f p
qed

实例化全称命题

在依赖应用中,T-App 里的替换会做实际的工作。令 id=λ(P:Sort 0). λ(h:P). h\mathit{id} = \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}} 为第一个例子中的证明,并令 Γ=Q:Sort 0\Gamma = Q : \Sort{0}。那么 Infer(Γ,id  Q)\Infer(\Gamma, \mathit{id}\;Q):

  • Infer(Γ,  id  Q)\Infer(\Gamma,\; \mathit{id}\;Q) T-App
    • Infer(Γ,  id)\Infer(\Gamma,\; \mathit{id}) 返回 Π(P:Sort 0). Π(h:P). P\tPi{P}{\Sort{0}}{\tPi{h}{P}{P}},已经是一个 Π\Pi T-Lam
    • Check(Γ,  Q,  Sort 0)\Check(\Gamma,\; Q,\; \Sort{0}) 成功 T-Var
    • 返回 (Π(h:P). P)[P↦Q]=Π(h:Q). Q\subst{(\tPi{h}{P}{P})}{P}{Q} = \tPi{h}{Q}{Q},即 Q→QQ \to Q

从逻辑上看,这是 ∀\forall 消去:从“每个命题都蕴涵自身”得到“QQ 蕴涵 QQ”。结果的类型取决于参数 QQ 的值,这正是依赖 Π\Pi 类型所表达的东西,也是 T-App 要做替换的原因。在浏览器中,id\mathit{id} 是一个已定义的常量,QQ 来自上下文;T-App 节点上的注释记录了这次替换。

def id : forall (P : Sort 0), P -> P := fun (P : Sort 0) => fun (h : P) => h

转换

回忆第 3 章中的 Id=λ(X:Sort 0). X\mathit{Id} = \tLam{X}{\Sort{0}}{X}。核心系统没有定义,所以 Id  P\mathit{Id}\;P 是项 (λ(X:Sort 0). X)  P(\tLam{X}{\Sort{0}}{X})\;P 的简写。考虑检查

t=λ(P:Sort 0). λ(h:Id  P). hagainstΠ(P:Sort 0). P→P.t = \tLam{P}{\Sort{0}}{\tLam{h}{\mathit{Id}\;P}{h}} \quad\text{against}\quad \tPi{P}{\Sort{0}}{P \to P}.

Infer\Infer 首先检查标注 Id  P\mathit{Id}\;P 是一个类型:InferSort(Γ1,Id  P)\InferSort(\Gamma_1, \mathit{Id}\;P) 进入 Infer\Infer 的应用分支,函数 λ(X:Sort 0). X\tLam{X}{\Sort{0}}{X} 的类型是 Π(X:Sort 0). Sort 0\tPi{X}{\Sort{0}}{\Sort{0}},参数 PP 通过 Check(Γ1,P,Sort 0)\Check(\Gamma_1, P, \Sort{0}),结果是 Sort 0[X↦P]=Sort 0\subst{\Sort{0}}{X}{P} = \Sort{0}。所以 Id  P\mathit{Id}\;P 是一个类型,而且是一个命题。接着体 hh 的类型是 Id  P\mathit{Id}\;P,于是

Infer(⋅,t)  =  Π(P:Sort 0). Π(h:Id  P). Id  P.\Infer(\cdot, t) \;=\; \tPi{P}{\Sort{0}}{\tPi{h}{\mathit{Id}\;P}{\mathit{Id}\;P}}.

仅靠给约束变量改名,无法把它变成 Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P}。但 IsDefEq\IsDefEq 在比较之前会先计算两边,而 Id  P→βP\mathit{Id}\;P \betared P,所以推断出的类型变为 Π(P:Sort 0). Π(h:P). P\tPi{P}{\Sort{0}}{\tPi{h}{P}{P}},比较像第一个例子一样成功。在类型规则中,这一步是在推导根部使用一次 T-Conv:

⋅⊢t:Π(P:Sort 0). Id  P→Id  P⋅⊢Π(P:Sort 0). P→P:Sort 0Π(P:Sort 0). Id  P→Id  P≡Π(P:Sort 0). P→P⋅⊢t:Π(P:Sort 0). P→P;(T-Conv)\dfrac{ \cdot \vdash t : \tPi{P}{\Sort{0}}{\mathit{Id}\;P \to \mathit{Id}\;P} \qquad \cdot \vdash \tPi{P}{\Sort{0}}{P \to P} : \Sort{0} \qquad \tPi{P}{\Sort{0}}{\mathit{Id}\;P \to \mathit{Id}\;P} \conv \tPi{P}{\Sort{0}}{P \to P} }{\cdot \vdash t : \tPi{P}{\Sort{0}}{P \to P}}\rulename{T-Conv}

算法从不决定在哪里应用 T-Conv:正如第 5 章所述,转换隐藏在对 IsDefEq\IsDefEq 的调用中,而它只在比较两个类型的地方运行。

实时推导显示了计算发生的位置:标注 Id  P\mathit{Id}\;P 由 T-App 检查,推断出的类型一直含有 Id  P\mathit{Id}\;P,直到 Check\Check 把它与目标作比较。

# h 的标注是 (fun (X : Sort 0) => X) P,它计算到 P。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : (fun (X : Sort 0) => X) P) => h
qed

练习

像上面的例子那样,手工对这些项运行 Infer\Infer,是检验自己是否理解该算法的好办法。每一题也都可以在演练场中尝试。

证明 Π(P:Sort 0). Π(Q:Sort 0). P→Q→P\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{P \to Q \to P}}。

解答

一个答案是 λ(P:Sort 0). λ(Q:Sort 0). λ(p:P). λ(q:Q). p\tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{p}{P}{\tLam{q}{Q}{p}}}}。这个证明从不使用 qq:系统允许假设不被使用。

证明蕴涵是传递的: Π(P:Sort 0). Π(Q:Sort 0). Π(R:Sort 0). (P→Q)→(Q→R)→P→R\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{\tPi{R}{\Sort{0}}{(P \to Q) \to (Q \to R) \to P \to R}}}。

解答

一个答案以 λ(f:P→Q). λ(g:Q→R). λ(p:P). g  (f  p)\tLam{f}{P \to Q}{\tLam{g}{Q \to R}{\tLam{p}{P}{g\;(f\;p)}}} 结尾;它两次用到应用分支,先是 f  p:Qf\;p : Q,然后是 g  (f  p):Rg\;(f\;p) : R。

goal forall (P : Sort 0), forall (Q : Sort 0), forall (R : Sort 0), (P -> Q) -> (Q -> R) -> P -> R
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (R : Sort 0) => fun (f : P -> Q) => fun (g : Q -> R) => fun (p : P) => g (f p)
qed

解释为什么 λ(P:Sort 0). λ(h:P). h  h\tLam{P}{\Sort{0}}{\tLam{h}{P}{h\;h}} 会被拒绝,以及在算法 5.1 的哪一行被拒绝。

解答

在上下文 P:Sort 0,h:PP : \Sort{0}, h : P 中,体 h  hh\;h 进入应用分支,Infer\Infer 对函数 hh 返回 PP,而 Whnf(P)=P\Whnf(P) = P 是一个变量,不是 Π\Pi。算法在 else fail "not a function" 这一行失败:PP 的证明不是可以被应用的东西。在 Rust 中,错误信息是 application of non-function。

要点回顾

  • 一次 Infer\Infer 的运行对应一棵推导树;每一次返回的调用就是一个节点。
  • Infer\Infer 可以在一个并未证明所声明定理的项上成功,对照所声明的类型运行 Check\Check 才能抓出这种情况。
  • T-App 是使用假设(肯定前件)的地方,也是实例化全称命题(替换进 Π\Pi 的体)的地方。
  • 类型内部的计算完全由 IsDefEq\IsDefEq 处理;算法从不选择在哪里应用 T-Conv。