求值顺序与CPS
最近在读小林直树写的Model checking higher-order programs时发现了一个错误。在把 call-by-name的
x
转换成call-by-value的
λk.k x
时,他把式子写成了λx.k x。要说我是怎么发现这个错误的,这其实是因为一开始我完全没有看懂什么叫做可以把call-by-value的程序转换成顶层全是unit类型的call-by-name程序。于是我就去查了Plotkin的原始文献,发现了上面的错误。
正确来说,用于转换的公式有以下四个(用unary算符⋅表示转换)
- a=λk.k a,其中a是常量
- x=λk.k x,其中x是变量
- λx.M=λk.k (λx.M)
- MN=λk.(M(λfN(λa.f a k)))
话说Plotkin居然会把κ和x混起来用,在论文的样式下这两个字符简直一模一样。1975年的文献还只有影印版,真是视力毁灭者。也难怪小林直树会把这两个字符搞混了。
Call-by-value, Call-by-name和λ演算
在讨论一些更深入的事情之前,有必要解释一下λ calculus里的cbv和cbn。粗略地讲,在程序语言中,call-by-value指在进行函数参数传递时所有参数会被首先求值,最后把值传进函数;而call-by-name则会把表达式直接传进去,直到完全传透为止不进行表达式的求值。
在λ演算中的call-by-value和call-by-name则按照如下方式定义,注意下面值指的是非application的表达式(即常量,变量和抽象)
- call-by-value:仅允许值进行代入;
- call-by-name:从最外层最左侧的表达式开始求值(代入)。
我们知道,根据Church-Rossar定理,一个λ表达式的最简式和它的代入顺序无关(当然,如果“不恰当地”选择顺序,式子可能无法化到最简)。因此无论是cbv还是cbn,对于同一个λ表达式,最终的结果总是一样的。但是这个结论不总是成立。其一,存在上述的规约顺序的问题,这会影响规约最终是否能够终止;其二,如果λ演算允许副作用存在,则可能出现不同的结果。一个程序例子如下
let x := 0 in
let f a := a + a in
f (x := !x + 1; !x)
在这个例子里,call-by-value的最终结果是2,而call-by-name的最终结果是3。一般而言, call-by-name会获得更好的终止性,而call-by-value则更符合机器执行逻辑。一个只在 call-by-name的情况下可以终止规约的λ表达式如下:
(λx.y)((λx.x x)(λx.x x))→V(λx.y)((λx.x x)(λx.x x))
(λx.y)((λx.x x)(λx.x x))→Ny
转换
在小林直树的文章里,他使用了并不非常正式的表述“转换”。严格来说,在被转换之前和之后,程序始终是λ表达式,转换并不足以表达前后的某种等价关系。在原始文献中,有更加严密的表达。
Our object here is to show that call-by-value can be simulated by call-by-name, and vise versa.
作者使用了“模拟”。也就是说,用call-by-name的方式对转换后的程序进行规约时,能够得到和用call-by-value方式对转换前程序进行规约时的同样行为。作者用三个定理描述了这个性质。
- Theorem1. (Indifference). EvalN(M(λx.x))=EvalV(M(λx.x))
- Theorem2. (Simulation). Ψ(EvalV(M))=EvalN(M(λx.x))
- Theorem3. (Translation). If λVL⊢M=N then λVL′′⊢M=N and then λN⊢M=N . The second but not the first implication is reversible.
其中L指call-by-value的原语言,L′指call-by-name的语言,L′′指call-by-value的语言,但是其常量,变量,变量列表和Constapply使用了call-by-name的版本。其中Constapply是一个应用primitive函数的语法糖,其中Constapply(a,b)指把primitivea应用到b上,若无法把a应用到b上,或a不是primitive,则Constapply(a,b)没有定义,有以下定义
ConstapplyN(a,b)=ConstapplyV(a,b)
Ψ(x)定义为
Ψ(a)=a, Ψ(x)=x, Ψ(λx.M)=λx.M.
即保存所有抽象,值和变量,只改变应用。
不过在深入讨论(并证明)这三个定理之前,我们先来非正式地思考一下λ式规约结果的等价性。让我们回到最开头的转换公式上。
- a=λk.k a,其中a是常量
- x=λk.k x,其中x是变量
- λx.M=λk.k (λx.M)
- MN=λk.(M(λf.N(λa.f a k)))
可以观察到,从构成上来说,前三个公式用了一样的转换方式:接受一个k,然后对k应用原式。这里用到的是(CPS, continuation-passing style)的思路。k是被显式编码的continuation计算,即“执行完a之后会执行的控制流”。在直觉上令人有些迷惑的是,原式被放在了continuation后面,这看起来像是“先执行continuation,然后再执行原式”,例如
(λk.k x)K→K x
关于K,一般地,如果表达式处于顶层,我们会把这个K定义成恒等的λx.x。这样,表达式才能够被真正执行,例如
(λk.k x)(λx.x)→(λx.x)x→x
而对于非顶层的表达式,在转换到call-by-name(L′)后,K也一定在开头携带一个抽象,这部分会把后面的x翻到前面去。
对于第四个转换公式,可以注意到它获取了3个continuation。第一个continuation k作为整体表达式的K,被放在了最后一个λ抽象的最后的位置,成为了f a的continuation。然而f,a似乎又与上面得出的反转直觉相反---它们又不反转了。这是因为M和N一定存在把它们各自的continuation翻到前面的λ抽象。例如
M=λk.k Y, N=λk.k Z,
MNK=(λk.k Y)(λk.k Z)K→((λk.k Z)Y)K→Y Z K
MNK=M(λf.N(λa.f a K))=(λk.k Y)(λf.N(λa.f a K))→(λf.N(λa.f a K))Y=(λf.(λk.k Z)(λa.f a K))Y→(λf.(λa.f a K)Z)Y→Y Z K
定理的证明
Plotkin用4个引理证明了这3个定理。让我们从引理一开始。
引理一
Lemma 1. [Ψ(N)/x]M=[N/x]M, if N is a value and x∈{k,a,b}.
这个引理相对比较好证明,只需对M进行结构性归纳即可。
- 若M是常量a,则有
[Ψ(N)/x]a=[Ψ(N)/x](λk.k a)=(λk.k a)=[N/x]a
- 若M是变量,则有
[Ψ(N)/x]x=[Ψ(N)/x](λk.k x)=λk.k Ψ(N) (k∈FV(N))=N=[N/x]x
- 若M是抽象λy.M1,则有
[Ψ(N)/x]λy.M1=[Ψ(N)/x](λk.k (λy.M1))=λk.k (λz.([Ψ(N)/x][z/y]M1))(x=k, k∈FV(N), and with the usual condition on z)=λk.k (λz.([Ψ(N)/x][z/y]M1))(by induction hypothesis)=λk.k (λz.([N/x][z/y]M1))(by induction hypothesis)=λz.([N/x][z/y]M1)=[N/x]λy.M1
这里有一步可能会造成困扰。第二个等号引入了新的抽象z。从表面上看,好像更简单的写法是λk.k (λy.([Ψ(N)/x]M1)),y∈FV(Ψ(N)).不这么写是因为,由于M中本身就出现了y,FV(Ψ(N))可能真的会有y,使得这种写法不一定总是成立。
- 若M是应用M1M2,则有
M1M2=[Ψ(N)/x](λk.M1(λf.M2(λa.f a k)))=λk.[Ψ(N)/x]M1(λf.[Ψ(N)/x]M2(λa.f a k))(as x∈{k,f,a} and f,a∈FV(Ψ(N)))=λk.[N/x]M1(λf.[N/x]M2(λa.f a k))(by induction hypothesis)=[N/x]M1[N/x]M2=R.S.
引理二
在介绍引理二之前,Plotkin引入了一个新的算符“:”。它定义在L×L′→L′上,具体如下
N:K=KΨ(N)MN:K=M:(λf.N(λa.f a K))MN:K=N:(λa.Ψ(M) a K)MN:K=Ψ(M)Ψ(N)K(N is a closed value)(M is not a value)(M, but not N, is a value)(M and N are values)
在此基础上,我们有引理2
Lemma 2. If K is a closed value then MK→+M:K, for any term M,(f,a,k∈FV(K)).
其中→+表示一步及以上但有限的规约。引理二也通过结构性归纳证明。
- 若M是值(即不是应用),那么
MK=(λk.kΨ(M))K→KΨ(M)=M:K
- 若M是应用(M1M2),那么
- subcase 1:若M1不是值,那么根据“:”的定义有
(M1M2):K=M1:(λf.M2(λa.f a K))
而
MK=M1M2K=(λk.M1(λf.M2(λa.f a k)))K→M1(λf.M2(λa.f a K))→+M1:(λf.M2(λa.f a K))(by induction hypothesis)=M:K
- subcase 2:若M1是值,但M2不是值,那么有
(M1M2):K=M2:(λa.Ψ(M1) a K)
而
MK→+M1:(λf.M2(λa.f a K))(by induction step in subcase 1)=(λf.M2(λa.f a K))Ψ(M1)→M2(λa.Ψ(M1) a K)→+M2:(λa.Ψ(M1) a K)(by induction hypothesis)=M:K
- subcase 3:若M1和M2都是值,那么
(M1M2):K=Ψ(M1)Ψ(M2)K
而
MK→+M2:(λa.Ψ(M1) a K)(as in subcase 2)→(λa.Ψ(M1) a K)Ψ(M2)→Ψ(M2)Ψ(M1)K
这样就完成了这个引理的证明。原始文献里有些跳步让它变得难读,上面做了一些修正。
引理三
引理三如下
Lemma 3. If MV→N then M:K→+N:K ( If K is a closed value and M and N are terms).
同样地,我们使用结构性归纳。这里我们按照M及规约后的N的类型进行分类。注意M一定是某种应用。
- subcase 1:M是常量应用ab,此时N立即地是ConstapplyV(a,b),有
M:K=abK→ConstapplyN(a,b)K=NK→+N:K(by Lemma 2)
- subcase 2:M是抽象应用(λx.M1)M2且M2是值(即不是应用),因此N是[M2/x]M1,有
M:K=Ψ(λx.M1)Ψ(M2)K(by :’s definition)=(λx.M1)Ψ(M2)K→[Ψ(M2)/x]M1K=[M2/x]M1K(M2 is a value, hence by Lemma 1)=NK→+N:K(by Lemma 2)
- subcase 3:M是一般应用M1M2,且M1是值,M2有规约M2V→N2,此时可得N=M1N2,有
M:K=M2:(λa.Ψ(M1) a K)(by :’s definition)→+N2:(λa.Ψ(M1) a K)(by induction hypothesis)=L,
- 若N2是值,首先
N:K=(M1N2):K=Ψ(M1)Ψ(N2)K
而根据:的定义,可得
L=(λa.Ψ(M1) a K)Ψ(N2)→Ψ(M1)Ψ(N2)K=N:K
- 若N2不是值,根据:的定义
N:K=(M1N2):K=N2:(λa.Ψ(M1) a K)
直接得到了L。
- subcase 4:M是一般应用M1M2,且M1有规约M1V→N1,此时可得N=N1M2,那么
M:K=(M1M2):K=M1:(λf.M2(λa.f a K))(by :’s definition)→+N1:(λf.M2(λa.f a K))(by induction hypothesis)=L,
- 若N1是值,根据:的定义,有
L=(λf.M2(λa.f a K))Ψ(N1)→M2(λa.Ψ(N1) a K)→+M2:(λa.Ψ(N1) a K)(by Lemma 2)=L′
- 此时考虑M2,若M2是值,根据“:”的定义有
N:K=N1M2:K=Ψ(N1)Ψ(M2)K
且
L′=(λa.Ψ(N1) a K)Ψ(M2)→Ψ(N1)Ψ(M2)K=N:K
- 而若此时M2不是值,根据“:”的定义有
N:K=N1M2:K=M2:(λa.Ψ(N1) a K)
直接得到了L′。
- 若N1不是值,根据:的定义有
N:K=N1M2:K=N1(λf.M2(λa.f a K))
直接得到了L。
这样就完成了证明。不得不说原始证明写得真是烂透了。
引理四
在引理四的开头,作者首先定义了Sticks∗。一个很重要但是作者却没有明说的直觉是,Sticks∗指在∗的求值策略下最终会卡住而无法进一步化简的表达式。SticksV定义如下
- 常量应用:若ConstapplyV(a,b)没有定义(即a不是可以应用于b的primitive),则ab∈SticksV
- 常量-抽象应用:对于任意项N,a(λx.N)∈SticksV
- 抽象应用:若N∈SticksV,那么对于任意项M,有(λx.M)N∈SticksV
- 一般应用:若M∈SticksV,那么对于任意项N,有MN∈SticksV
作者只分了四类令人有些意外。按照作者的定义,一个项可以是常量,变量,抽象和应用。即使顶层是变量x没有意义,
而SticksN定义如下
- 常量应用:若ConstapplyN(a,b)没有定义(即a不是可以应用于b的primitive),则ab∈SticksN
- 常量-抽象应用:对于任意项N,a(λx.N)∈SticksN
- 一般应用:若M∈SticksN,那么对于任意项N,有MN∈SticksN
- 常量-一般应用:若N∈SticksN,那么aN∈SticksN
在此基础上,有
- 若MV→+N,且M和N都不是值(都是应用),且M内没有自由变量,那么M∈SticksV
- 若MN→+N,且M和N都不是值,且M内没有自由变量,那么M∈SticksN
作者指出显然SticksV⊆SticksN。