本章在几个小项上手工运行算法 5.1,并把每一次运行与类型规则对应起来。所选的例子覆盖了算法的各项工作:构造推导、抓出错误的证明、使用假设、实例化全称命题,以及在类型内部进行计算。下面的每一条追踪都可以在演练场中用内核复现。
要验证 t 是定理 T 的证明,内核调用 Check(⋅,t,T);它调用 Infer(⋅,t),再把结果与 T 作比较。追踪展示了 Infer 发出的递归调用,以及每次调用所对应的类型规则。展开或折叠各节点即可跟随递归过程。
恒等证明
令 Γ1=P:Sort0,Γ2=Γ1,h:P。第 1 章开篇例子的推导,略去良构性前提后,是
⋅⊢λ(P:Sort0).λ(h:P).h:Π(P:Sort0).P→P⋅⊢Sort0:Sort1;(T-Sort)Γ1⊢λ(h:P).h:P→PΓ1⊢P:Sort0;(T-Var)Γ2⊢h:P;(T-Var);(T-Lam);(T-Lam)
算法自下而上构造出同一棵推导。调用 Infer(⋅,λ(P:Sort0).λ(h:P).h) 的过程如下。
- Infer(⋅,λ(P:Sort0).λ(h:P).h) T-Lam
- InferSort(⋅,Sort0):Infer 返回 Sort1,所以标注是一个类型 T-Sort
- Freshen 保持 P 不变,因为上下文为空
- Infer(Γ1,λ(h:P).h) T-Lam
- InferSort(Γ1,P):Infer 查找 P 并返回 Sort0 T-Var
- Freshen 保持 h 不变
- Infer(Γ2,h) 查找 h 并返回 P T-Var
- 返回 Π(h:P).P
- 返回 Π(P:Sort0).Π(h:P).P
每一次返回类型的调用都对应推导树的一个节点,而且没有任何调用用到计算。最后,Check 把结果与所声明的类型 Π(P:Sort0).P→P 作比较。两者只差一个约束变量的名字,因为 P→P 是 Π(_:P).P 的缩写,而 h 和 _ 都不出现在体中。于是 IsDefEq 成功,证明被接受。
下面是由内核自己记录的同一次运行:调用视图与上面的列表一致,证明树视图与推导一致。
# 同一个证明,以完整的项给出。内核推断它的类型,
# 再与目标作比较。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => h
qed
一个错误的证明
假设用户声称 t=λ(P:Sort0).λ(h:P).P 证明了 Π(P:Sort0).P→P。它的体返回的是命题 P,而不是证明 h。内核调用 Check(⋅,t,Π(P:Sort0).P→P),后者先调用 Infer,各次调用与上面相同,只是最后一次 Infer(Γ2,P) 返回 Sort0。于是 Infer 成功,结果为
Infer(⋅,t)=Π(P:Sort0).Π(h:P).Sort0.
项 t 是良类型的,却不能证明所声称的命题。Check 比较这两个类型时,发现两边都无法继续归约,重命名约束变量也不能使它们相等:最内层的体一边是 Sort0,另一边是 P。因此 IsDefEq 失败,Check 报告类型不匹配。(第 11 章用 IsDefEq 的算法逐步重演这次比较。)
这个错误之所以能被发现,只是因为用户声明了要证明的命题,而一个只运行 Infer 的内核会接受 t,并悄悄给它分配一个不同的类型。这就是为什么每个脚本都以 goal 开头,也是为什么 qed 要对照它来检查。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => P
qed
使用假设:肯定前件
到目前为止的例子从未应用过函数。考虑检查
t=λ(P:Sort0).λ(Q:Sort0).λ(f:P→Q).λ(p:P).fpagainstΠ(P:Sort0).Π(Q:Sort0).(P→Q)→P→Q,
它说的是“如果 P 蕴涵 Q 且 P 成立,那么 Q 成立”。四个 λ 像之前一样把上下文扩展为 Γ=P:Sort0,Q:Sort0,f:P→Q,p:P。体 fp 进入 Infer 的应用分支:
- Infer(Γ,fp) T-App
- Infer(Γ,f) 返回 P→Q,即 Π(_:P).Q。Whnf 保持它不变,它是一个 Π T-Var
- Check(Γ,p,P) 成功,因为 p:P 在 Γ 中 T-Var, T-Conv
- 返回 Q[_↦p]=Q。名字 _ 不出现在 Q 中,所以替换什么也不做
如果用户写的是 ff,第二步就会调用 Check(Γ,f,P),它会失败,因为 P→Q 不是 P:在这里函数不能喂给它自己。试一试:把下面的 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:Sort0).λ(h:P).h 为第一个例子中的证明,并令 Γ=Q:Sort0。那么 Infer(Γ,idQ):
- Infer(Γ,idQ) T-App
- Infer(Γ,id) 返回 Π(P:Sort0).Π(h:P).P,已经是一个 Π T-Lam
- Check(Γ,Q,Sort0) 成功 T-Var
- 返回 (Π(h:P).P)[P↦Q]=Π(h:Q).Q,即 Q→Q
从逻辑上看,这是 ∀ 消去:从“每个命题都蕴涵自身”得到“Q 蕴涵 Q”。结果的类型取决于参数 Q 的值,这正是依赖 Π 类型所表达的东西,也是 T-App 要做替换的原因。在浏览器中,id 是一个已定义的常量,Q 来自上下文;T-App 节点上的注释记录了这次替换。
def id : forall (P : Sort 0), P -> P := fun (P : Sort 0) => fun (h : P) => h
转换
回忆第 3 章中的 Id=λ(X:Sort0).X。核心系统没有定义,所以 IdP 是项 (λ(X:Sort0).X)P 的简写。考虑检查
t=λ(P:Sort0).λ(h:IdP).hagainstΠ(P:Sort0).P→P.
Infer 首先检查标注 IdP 是一个类型:InferSort(Γ1,IdP) 进入 Infer 的应用分支,函数 λ(X:Sort0).X 的类型是 Π(X:Sort0).Sort0,参数 P 通过 Check(Γ1,P,Sort0),结果是 Sort0[X↦P]=Sort0。所以 IdP 是一个类型,而且是一个命题。接着体 h 的类型是 IdP,于是
Infer(⋅,t)=Π(P:Sort0).Π(h:IdP).IdP.
仅靠给约束变量改名,无法把它变成 Π(P:Sort0).P→P。但 IsDefEq 在比较之前会先计算两边,而 IdP→βP,所以推断出的类型变为 Π(P:Sort0).Π(h:P).P,比较像第一个例子一样成功。在类型规则中,这一步是在推导根部使用一次 T-Conv:
⋅⊢t:Π(P:Sort0).P→P⋅⊢t:Π(P:Sort0).IdP→IdP⋅⊢Π(P:Sort0).P→P:Sort0Π(P:Sort0).IdP→IdP≡Π(P:Sort0).P→P;(T-Conv)
算法从不决定在哪里应用 T-Conv:正如第 5 章所述,转换隐藏在对 IsDefEq 的调用中,而它只在比较两个类型的地方运行。
实时推导显示了计算发生的位置:标注 IdP 由 T-App 检查,推断出的类型一直含有 IdP,直到 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,是检验自己是否理解该算法的好办法。每一题也都可以在演练场中尝试。
证明 Π(P:Sort0).Π(Q:Sort0).P→Q→P。
解答
一个答案是 λ(P:Sort0).λ(Q:Sort0).λ(p:P).λ(q:Q).p。这个证明从不使用 q:系统允许假设不被使用。
证明蕴涵是传递的:
Π(P:Sort0).Π(Q:Sort0).Π(R:Sort0).(P→Q)→(Q→R)→P→R。
解答
一个答案以 λ(f:P→Q).λ(g:Q→R).λ(p:P).g(fp) 结尾;它两次用到应用分支,先是 fp:Q,然后是 g(fp):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:Sort0).λ(h:P).hh 会被拒绝,以及在算法 5.1 的哪一行被拒绝。
解答
在上下文 P:Sort0,h:P 中,体 hh 进入应用分支,Infer 对函数 h 返回 P,而 Whnf(P)=P 是一个变量,不是 Π。算法在 else fail "not a function" 这一行失败:P 的证明不是可以被应用的东西。在 Rust 中,错误信息是 application of non-function。
要点回顾
- 一次 Infer 的运行对应一棵推导树;每一次返回的调用就是一个节点。
- Infer 可以在一个并未证明所声明定理的项上成功,对照所声明的类型运行 Check 才能抓出这种情况。
- T-App 是使用假设(肯定前件)的地方,也是实例化全称命题(替换进 Π 的体)的地方。
- 类型内部的计算完全由 IsDefEq 处理;算法从不选择在哪里应用 T-Conv。