.
确定性:从规则归纳到求值器
. 确定性:从规则归纳到求值器
用规则归纳法证明一步规约的确定性,并证明 Rust 求值函数与规约规则一致.
是对规则封闭的最小关系,因此具有规则归纳法. 它与 结构归纳原理 的依据相同: 每个可推导的 都来自有限次规则应用; 只要待证性质在无前提的规则上成立,并能由每条规则的前提传到结论, 就对所有可推导的转移成立.
直观地说,规则归纳就是对推导树的结构或高度归纳, 请对照 一棵推导 理解. 公理对应没有前提的叶子,是基准情况, 没有 归纳假设 可用; E-If 这类带前提的规则对应归纳情况, 可以对前提的子推导使用归纳假设,再证明当前结论的性质.
这里需要对推导归纳(单对项作结构归纳不够用),因为已知条件是一棵 的推导,要跟踪的性质同时涉及左右两项; 按最后用的规则拆解,就能得到前提里的子推导以及左右两边怎样拼成结论. 确定性和保型都会用到它. 若只试了几条规约链,或只处理无前提的基本规约规则而漏掉 E-If 等结构规则, 就不能覆盖嵌套项:例如内层规约保持类型,并不替你证明外层 拼回去以后也保持类型. 规则归纳正是把这个“拼回去”的步骤逐条核实.
在证明确定性之前,要先确认“已经算完的值”确实不能再走一步. 这既给求值器提供了可靠的停止条件,也用来排除基本规约规则和结构规则同时适用. 若另加 这样的规则, 虽仍是语法上指定的值, 按“不能再走”判断停止的求值器却会永远循环; 后面的确定性证明也不能再用“值不能规约”排除重叠.
接下来证明一步规约的确定性,为后面的求值函数铺路: 我们准备实现的
step
step
每次只返回单个后继项; 它要完整实现一步规约关系,就必须证明关系不会给同一输入两个不同后继, 否则
match
match
的分支顺序可能偷偷选掉另一条合法路线.
E-PredSucc 的数值限制
会给出放宽一条规则就失去这一保证的具体反例; 多路线不必然是语言设计错误,但必须说明是否用策略限制它 (
求值策略、规约策略与合流性
).
这个证明没什么巧思,功夫全花在排除情形上. 这正是形式化的用处,下面的附注给出具体例子.
5 从关系到函数
确定性
表明:对每个词项 ,至多有一个一步后继 . 因此 对应偏函数:有后继时返回唯一后继,没有后继时无定义. 用 Rust 实现时,可以把返回类型写成
Option<Term>
Option<Term>
,以
None
None
显式表示没有后继. 这样能把数学上的偏函数扩充为词项到可选结果的全函数,前提是它对每个输入都终止; 仅有这个返回类型并不保证终止. 下面的
step
step
及
is_nv
is_nv
都沿直接子项递归,克隆也只处理有限子树, 所以在
算术语言形式化的未展开前提
的资源与基础操作假设下会正常返回. 若一步关系有多个后继,函数仍然可以选择其中一个,但必须写明选择的策略; 否则它只实现了关系的一部分,却误说成实现了整个关系.
先把前面的
Term
Term
定义重新贴在这里,方便对照:
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Term {
True, False,
If(Box<Term>, Box<Term>, Box<Term>), // if t1 then t2 else t3
Zero,
Succ(Box<Term>), Pred(Box<Term>), IsZero(Box<Term>),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Term {
True, False,
If(Box<Term>, Box<Term>, Box<Term>), // if t1 then t2 else t3
Zero,
Succ(Box<Term>), Pred(Box<Term>), IsZero(Box<Term>),
}
Term
Term
没有派生
Copy
Copy
.
Copy
Copy
的意思是“按位复制一份就是合法的副本”,而
Box
Box
独占堆上的子树; 按位复制会得到两个指向同一块内存、都以为自己负责释放它的指针, 所以 Rust 不允许含
Box
Box
的类型实现
Copy
Copy
. 下面代码里的
(**a).clone()
(**a).clone()
、
a.clone()
a.clone()
是在显式地深拷贝子树:
a
a
的类型是
&Box<Term>
&Box<Term>
,
*a
*a
是
Box<Term>
Box<Term>
,
**a
**a
是
Term
Term
.
/// nv ::= 0 | succ nv
pub fn is_nv(t: &Term) -> bool {
match t { Term::Zero => true, Term::Succ(t) => is_nv(t), _ => false }
}
/// v ::= true | false | nv
pub fn is_value(t: &Term) -> bool {
matches!(t, Term::True | Term::False) || is_nv(t)
}
/// t ⟶ t'.返回 None 表示不存在 t':t 是值,或者受阻了.
pub fn step(t: &Term) -> Option<Term> {
use Term::*;
match t {
If(c, a, b) => match &**c {
True => Some((**a).clone()), // E-IfTrue
False => Some((**b).clone()), // E-IfFalse
c => Some(If(Box::new(step(c)?), a.clone(), b.clone())), // E-If
},
Succ(t1) => Some(Succ(Box::new(step(t1)?))), // E-Succ
Pred(t1) => match &**t1 {
Zero => Some(Zero), // E-PredZero
Succ(n) if is_nv(n) => Some((**n).clone()), // E-PredSucc
t1 => Some(Pred(Box::new(step(t1)?))), // E-Pred
},
IsZero(t1) => match &**t1 {
Zero => Some(True), // E-IsZeroZero
Succ(n) if is_nv(n) => Some(False), // E-IsZeroSucc
t1 => Some(IsZero(Box::new(step(t1)?))), // E-IsZero
},
True | False | Zero => None, // 值不可规约
}
}
/// nv ::= 0 | succ nv
pub fn is_nv(t: &Term) -> bool {
match t { Term::Zero => true, Term::Succ(t) => is_nv(t), _ => false }
}
/// v ::= true | false | nv
pub fn is_value(t: &Term) -> bool {
matches!(t, Term::True | Term::False) || is_nv(t)
}
/// t ⟶ t'.返回 None 表示不存在 t':t 是值,或者受阻了.
pub fn step(t: &Term) -> Option<Term> {
use Term::*;
match t {
If(c, a, b) => match &**c {
True => Some((**a).clone()), // E-IfTrue
False => Some((**b).clone()), // E-IfFalse
c => Some(If(Box::new(step(c)?), a.clone(), b.clone())), // E-If
},
Succ(t1) => Some(Succ(Box::new(step(t1)?))), // E-Succ
Pred(t1) => match &**t1 {
Zero => Some(Zero), // E-PredZero
Succ(n) if is_nv(n) => Some((**n).clone()), // E-PredSucc
t1 => Some(Pred(Box::new(step(t1)?))), // E-Pred
},
IsZero(t1) => match &**t1 {
Zero => Some(True), // E-IsZeroZero
Succ(n) if is_nv(n) => Some(False), // E-IsZeroSucc
t1 => Some(IsZero(Box::new(step(t1)?))), // E-IsZero
},
True | False | Zero => None, // 值不可规约
}
}
每个分支旁边标了它实现的规则. 结构规则对应的分支都用到了
?
?
,它的作用见下面的附注.
确定性说明关系至多有一个后继,但还没有说明
step
step
返回的就是这个后继. 下面的定理把两者接上: 函数返回的每个后继都能由规则推出,规则允许的每个后继函数也都能返回.