. 求值:用推导规则定义运行

定义算术语言的值与一步规约关系,区分基本规约规则与结构规则.

上一节定义词项的语法:它由哪些构造子、按什么结构组成. 这一节定义词项的语义,也就是在给定模型中的意义. 程序语言语义学研究怎样赋予程序精确的数学意义.

本文采用操作语义:通过执行状态及其执行关系描述程序行为. 具体采用的是小步操作语义,每次转移描述一步执行. 在这门语言中,执行状态就是词项,不需要另加存储或环境. 不完整的规格 里“先算哪边”的问题,将由转移规则规定; 规则是否保证下一步唯一,还须另行证明.

Remark 1 存在与生成 [being-and-becoming]

2 值

先划出“已经算完”的项.

Definition 2.1 值与数值 [values-and-numeric-values]

在 词项的集合 的词项中,用两行 BNF 文法划出值. ⩴ 读作“定义为”,| 读作“或者”; nv 是元语言中表示数值的元变量,不属于对象语言的关键字.

v ⩴ true | false | nv
nv ⩴ 0 | succ nv

形如 𝑣 的词项称为值,形如 nv 的词项称为数值. 和 词项的集合 一样, 这两行 BNF 定义的是满足相应封闭条件的最小集合.

数值就是 0、succ 0、succ ( succ 0 )……,分别代表 0,1,2,…. 例如 true、0、succ ( succ 0 ) 都是值; succ  true 虽然是项,但由于 succ 后面要求数值而 true 不属于数值,它算不上值; pred 0 也不属于值(值的定义里根本没有 pred,直观上它还能再算).

3 推导规则怎么读

规约是依据规则把词项改写为另一词项, 例如把 ( 1+2 )×3 改写为 3×3,再改写为 9. 一次改写称为一步规约,连续零次或有限多次改写称为多步规约. 规约不天然保证词项更小或更接近终点;本文的终止性要由大小严格下降另行证明.

在本模型中,⟶ 同时是词项之间的转移关系: 𝑡⟶𝑡 ′ 描述从状态 𝑡 到状态 𝑡 ′ 的一次转移. 程序执行就是依次作这样的转移.

Convention 3.1 箭头 ⟶ 的读法与用法 [reading-the-reduction-arrow]

这里基于 词项的集合 中的项与 一步规约 定义的关系 ⟶,约定箭头的读法.

𝑡⟶𝑡 ′ 读作“𝑡 一步规约到 𝑡 ′”,也可读作“从状态 𝑡 转移一步到状态 𝑡 ′”. 它是关于转移的判断,断言这对词项属于所定义的关系; 判断成立,当且仅当规则能给出有限推导. 例如 pred 0⟶0 成立,0⟶ pred 0 不成立.

箭头有方向,左边是规约前的项,右边是规约后的项. 𝑡⟶𝑡 ′ 只陈述两项满足这一关系,不同于函数调用或原地修改 𝑡 的赋值. 这个记号本身不要求后继唯一;唯一性要由 确定性 另行证明. 带上星号的 ⟶ ∗ 读作“多步规约到”,含义由 多步规约 给出.

数学上,⟶ 是二元关系: 它是由若干 ( 𝑡,𝑡 ′ ) 对组成的集合,𝑡⟶𝑡 ′ 是 ( 𝑡,𝑡 ′ )∈⟶ 的简写. 我们用推导规则定义它.一条规则形如

前提  1⋯ 前提 𝑛 结论

读作:如果横线上的前提全都成立,那么横线下的结论成立. 没有前提的规则称为公理;它是推导规则的特例,不需要先证明其他判断就能使用.

横线右边可以标上规则的名字,方便引用.

Convention 3.2 元变量 [metavariables]

在 一步规约 与 类型与类型判断 的规则里, 𝑡 1,𝑡 2,𝑡 1 ′ 等字母是元变量: 它们属于元语言,可以代换为 词项的集合 中的任意词项; 对象语言自身没有变量. 𝑣 只能代换为值, nv 只能代换为数值 ( 值与数值 ), 𝑇 则只能代换为类型. 同一条规则中同一个元变量的所有出现必须使用相同的代换. 因此,一条推导规则概括了许多具体实例.

元变量的取值范围也是规则的一部分. 例如 nv 只能表示数值,不能用任意词项代换. 文献还常把额外限制显式写成附加条件,放在横线旁或前提中; 它们必须满足,才能应用规则,不能靠读者自行补全.

Definition 3.3 一步规约 [one-step-reduction]

以 词项的集合 的词项作为执行状态, nv 只取 值与数值 中的数值. 一步规约关系 ⟶,即此模型的小步转移关系, 是对下面十条推导规则封闭的最小二元关系. 𝑡⟶𝑡 ′ 成立,当且仅当存在以这个判断为结论的有限推导.

每条横线之上的判断是前提,横线之下的是结论;前提全部成立,结论才成立. 没有前提的规则称为公理,可以直接使用. 同一条规则中相同的元变量必须代换成同一个项,见 元变量 .

E-IfTrue:前提 无;结论 if true then t_(2) else t_(3) ⟶ t_(2) E-IfFalse:前提 无;结论 if false then t_(2) else t_(3) ⟶ t_(3)
E-If:前提 t_(1) ⟶ t'_(1);结论 if t_(1) then t_(2) else t_(3) ⟶ if t'_(1) then t_(2) else t_(3)
E-Succ:前提 t_(1) ⟶ t'_(1);结论 succ t_(1) ⟶ succ t'_(1) E-PredZero:前提 无;结论 pred 0 ⟶ 0 E-PredSucc:前提 无;结论 pred (succ nv) ⟶ nv E-Pred:前提 t_(1) ⟶ t'_(1);结论 pred t_(1) ⟶ pred t'_(1)
E-IsZeroZero:前提 无;结论 iszero 0 ⟶ true E-IsZeroSucc:前提 无;结论 iszero (succ nv) ⟶ false E-IsZero:前提 t_(1) ⟶ t'_(1);结论 iszero t_(1) ⟶ iszero t'_(1)

这里的“最小”和 词项的集合 里的“最小”含义相同: 𝑡⟶𝑡 ′ 成立,当且仅当能用这十条规则的实例搭出一棵以它为根的有限树. 这棵树称为推导树,也称推导. 推导规则是通用的规则模式, 推导则是把规则实例组合起来、证明具体判断的有限结构.

规则没有提到的情形,例如 succ  true 该规约到哪里,就是没有定义. “没有定义”在这里有精确的含义: 十条规则里没有一条的结论能匹配 succ  true ⟶… (E-Succ 的结论形状对得上,但它的前提要求 true 能规约,而没有规则能做到), 所以不存在任何 𝑡 ′ 使 succ  true ⟶𝑡 ′.

遇到没有定义的情形时,形式化将其直接视为明确的状态,常见处理方式有三种: 可以直接承认它是错误状态,把“不属于值、却又不能规约”的项单独命名(下文 正规形式与受阻 的“受阻”),再用类型系统证明良类型的程序永远到不了这种状态,本文采用这种做法; 也可以显式加上错误规则,扩充语法、增加终止计算的错误项 wrong,把原来受阻的运算规定为产生错误并向外传播,Python 对 1 + "a" 1 + "a" 抛出 TypeError TypeError 属于类似的运行时处理,Kotlin 的例子以及它与底类型的关系见 显式错误规则与 Kotlin 的底类型 ; 还可以补上约定的结果,例如规定布尔值先转成数,true 对应 1、false 对应 0,于是 succ  true ⟶ succ ( succ 0 ),JavaScript 的 true + 1 === 2 true + 1 === 2 就是这种做法(这只给该类运算规定了普通结果,不代表所有程序都会正常返回,隐式转换也可能掩盖本想发现的错误,见 JavaScript 隐式转换与相等三角图 ).

Note 3.4 显式错误规则与 Kotlin 的底类型 [explicit-errors-and-kotlin-bottom-type]

Example 3.5 JavaScript 隐式转换与相等三角图 [javascript-coercion-and-equality-triangle]

JavaScript 遇到 true + 1 true + 1 时明确规定把 true true 转成 1 1 再相加,得到 2 2 . 同样,宽松相等 == == 规定了按两边种类进行转换的规则; 严格相等 === === 则不做这种转换,不同种类的操作数直接判为不相等. 把两种比较放在一起看:

true + 1;    // 2:true 转换为 1

[] == 0;     // true
[] === 0;    // false
0 == "0";    // true
0 === "0";   // false
[] == "0";   // false
[] === "0";  // false
true + 1;    // 2:true 转换为 1

[] == 0;     // true
[] === 0;    // false
0 == "0";    // true
0 === "0";   // false
[] == "0";   // false
[] === "0";  // false

图中每条边同时记录 == == 和 === === 的比较结果(注意边代表相等比较,无方向,不同于规约箭头). 绿色实线表示 == == 为真,红色虚线表示 == == 为假; 第二行单独记录 === === ,这里三条边都为假.

[] == 0 [] == 0 会先把空数组转换成空字符串 "" "" ,再因另一边是数字而把 "" "" 转成 0 0 ; 0 == "0" 0 == "0" 会把字符串 "0" "0" 转成数字 0 0 ,所以这两条边都为真. 但 [] == "0" [] == "0" 把数组转成 "" "" 后,两边已经都是字符串,只比较 "" "" 与 "0" "0" ,结果为假. 具体转换规则见 MDN 的 == == 文档.

传递性要求:𝑎 与 𝑏 相等、𝑏 与 𝑐 相等,就能推出 𝑎 与 𝑐 相等. 这个三角图正好展示 == == 不满足传递性,不能像数学等号那样用于替换或推理. === === 省略了跨种类的类型转换:数组、数字、字符串种类不同,所以这三个比较都为假. 它和 == == 采用的是两套不同的判准.

即便如此, === === 仍与完整的数学等价关系有别. 自反性要求每个值都与自身相等,而 NaN === NaN NaN === NaN 为 false false . 对对象, === === 只看两边是否指向同一个对象,不比较内部内容: const a = []; a === a const a = []; a === a 为 true true , [] === [] [] === [] 却为 false false , 因为后者创建了两个不同的数组. 严格相等的完整规则见 MDN 的 === === 文档.

Note 3.6 相等关系的“地狱”给语言设计什么教训 [equality-and-language-design]

无论选哪种,关键是写下来. C 语言标准里的未定义行为是典型的反面教材: 标准明确说某些情形没有定义,却没有要求实现报错, 于是编译器可以假设它们永远不会发生,并据此做出让程序员意外的优化.

Example 3.7 一棵推导 [reduction-derivation]

使用 一步规约 的规则, 推导树的每条横线都是一条规则的实例: 横线上方的子推导满足前提,下方给出结论. 这棵三层的推导树合起来证明整项的一步规约(整棵树只对应一步转移,要与三步运行区分开).

E-Pred:前提 [E-If:前提 [E-IsZeroZero:前提 [无];结论 iszero 0 ⟶ true];结论 if (iszero 0) then succ 0 else 0 ⟶ if true then succ 0 else 0];结论 pred (if (iszero 0) then succ 0 else 0) ⟶ pred (if true then succ 0 else 0)

从下往上读:要说明最下面那一步成立,用 E-Pred,它要求里面的 if 能一步规约; 这又用 E-If,它要求条件 iszero 0 能一步规约; 最后 E-IsZeroZero 是公理,无条件成立.

4 规约规则与结构规则

这十条规则分成两类:一类规定基本运算怎样执行,另一类规定基本步骤可发生在哪些位置.

规约规则(也称计算规则或收缩规则)直接规定基本运算的结果: E-IfTrue、E-IfFalse、E-PredZero、E-PredSucc、E-IsZeroZero、E-IsZeroSucc. 能直接匹配这类规则的词项,称为可规约表达式,简称可规约项. 这里的六条基本规约规则没有关于 ⟶ 的前提(其他语言的规约规则未必都如此). 例如,取 𝑡 2= succ 0、𝑡 3=0,由 E-IfTrue 可得 if  true  then  succ 0 else 0⟶ succ 0; 取 nv = succ 0,由 E-PredSucc 可得 pred ( succ ( succ 0 ) )⟶ succ 0; 取 nv = 0,由 E-IsZeroSucc 可得 iszero ( succ 0 )⟶ false.

结构规则(也称同余规则)把子项的规约提升到指定的求值位置: E-If 允许规约条件,E-Succ、E-Pred、E-IsZero 允许规约各自的参数.

1在证明论与相继式演算中,“结构性规则”与“收缩规则”另指弱化、收缩、交换等操作语境的规则, 需与此处操作语义的同余规则和基本规约规则区分.
前提说那个位置的子项能规约,结论说整体能在该位置转移一步; 未列入规则的位置(如尚未选中的 if 分支)不能直接改写. 例如,由 E-PredZero 知 pred 0⟶0,再由 E-Succ 就得 succ ( pred 0 )⟶ succ 0; 由 E-IsZeroZero 知 iszero 0⟶ true,再由 E-If 就得 if ( iszero 0 ) then 0 else  succ 0⟶ if  true  then 0 else  succ 0.

这里的“同余”强调规约与特定构造相容,不表示一步规约是同余等价关系: ⟶ 有方向,而且只对规则许可的位置封闭. 把许可位置另写成带一个空位的求值语境,再规定怎样在空位处执行基本规约, 就得到语境语义(也称规约语义)的写法. 参见 Hutton 译文第 6 节,以及 Harper §1.3.1.

这套小步语义中,每棵一步推导都由零次或多次结构规则与顶端的一次基本规约规则组成. 从结论向上读,结构规则定位正在求值的子项,基本规约规则执行其中的运算. 一棵推导 就是两次结构规则(E-Pred、E-If) 加一次基本规约(E-IsZeroZero).

上图中 pred pred 和 if if 节点是结构规则经过的路径, iszero 0 iszero 0 是基本规约发生的位置; 虚线连接的分支不参与这一步.

规则的细节里藏着两项设计决定: E-If 只允许在条件里规约,没有规则允许在分支里规约, 因此总是先求条件、再选择分支,不会提前计算未选中的分支 (下文 求值策略、规约策略与合流性 会把这样的顺序规定称为求值策略,并与一般的规约策略区分); 同时,基本规约规则要求正在检查的操作数已经是合适的值,但不要求未选中的分支是值. 例如 E-PredSucc 的左边是 pred (succ nv), 只有 succ 里面已经是数值时才能用;否则只能先用结构规则把里面算完. 这保证了“先算里面,再算外面”的顺序, 其作用见下文 E-PredSucc 的数值限制 .

Remark 4.1 意义即使用 [meaning-as-use]