原文来自我的个人博客 twodam.net
前言
初次接触到Lambda calculus是在SICP ex2.6,题中涉及到Church numerals(丘奇数),然后看到了这几篇文章,当时看了几篇,看得不是很懂,搁置了。
我在学习SICP 4 Metalinguistic Abstraction,在做习题4.9的时候遇到一个lambda递归的问题,想到之前看的lambda演算中好像有提到,于是去找了相关论文,从其中选取了一个讲的比较清晰的入门介绍,算是加深了了解。
本篇算是我的笔记啦。
Reference:
Rojas R. A tutorial introduction to the lambda calculus[J]. arXiv preprint arXiv:1503.09060, 2015.
正文
定义
name 也叫作 variable,是一个标识符,如 abs;
function 是函数,如 λx.x; (在Scheme语言里,其常见的形式是 (lambda (x) (x))
)
application 是函数应用,如 E1E2, (λx.x)y;
递归定义如下:
<expression> := <name> | <function> | <application>
<function> := λ <name>.<expression>
<application> := <expression><expression>
有时候为了避免歧义会用括号包围表达式。
函数应用是左结合的,也就是说E1E2E3 . . . En
的求值是通过运算表达式(. . .((E1E2)E3). . . En)
。
函数应用是通过替换函数定义中参数的值来求值的,如(λx.x)y = [y/x]x = y
,这里[y/x]
表示用y来替换x,最终得到结果y。
函数定义中参数名字自身是没有意义的,它们只是用来指示当函数被求值时其参数应该如何变化的占位符。
因此(λz.z) ≡ (λy.y) ≡ (λt.t) ≡ (λu.u)
,这里≡
表示当A≡B
时A只是B的同义词。
自由和绑定变量
在lambda演算中,所有名字都是局部于定义的。
在λx.x
中x是绑定的,因为在定义体内它的前面是λx
。
一个前面不接着λ
的名字叫做“自由变量”。
在表达式(λx.xy)
中,x是绑定的,y是自由的。
在表达式(λx.x)(λy.yx)
,左侧第一个表达式中的x与第一个λ绑定,第二个表达式中的y与第二个λ绑定,而其中的x则是自由的。第二个表达式和第一个表达式中的x是完全不相关的。
Formally we say that a variable <name> is free in an expression if one of the following three cases holds:
• <name> is free in <name>.
• <name> is free in λ <name1 > . <exp> if the identifier <name>6=<name1> and <name> is free in <exp>.
• <name> is free in E1E2 if <name> is free in E1 or if it is free in E2.
A variable <name> is bound if one of two cases holds:
• <name> is bound in λ <name1 > . <exp> if the identifier <name>=<name1> or if <name> is bound in <exp>.
• <name> is bound in E1E2 if <name> is bound in E1 or if it is bound in E2.
替换
标准的lambda演算中,函数是没有名字的,当我们想要应用一个函数时,我们写下整个函数定义,然后紧接着对它进行求值。
为了简化操作,我们将使用大写字母,数字和其他符号作为函数定义的同义表达。
举个栗子,我们用I表示(λx.x)
II ≡ (λx.x)(λx.x)
其中两个表达式中的x是不相关的,那么我们可以将上述表达式重写为
II ≡ (λx.x)(λz.z)
结果是
[λz.z/x]x = λz.z ≡ I
执行替换时注意不要将一个标识符的自由形式和绑定形式混杂在一起了。在表达式(λx.(λy.xy))y
中,左侧的y是绑定的,而右侧的y是自由的,错误的替换将会产生一个错误的结果(λy.yy)
。
只是简单地将y重命名为t,我们就获得一个完全不同但是正确的答案(λx.(λt.xt))y = (λt.yt)
。
算术
如何表达算术运算?
在lambda演算中,我们只能定义新的函数。
我们可以从zero开始,suc(zero)表示1,suc(suc(zero))表示2,如此类推。
定义zero为 λs.(λz.z),对于多于一个参数的函数,这里缩写为 λsz.z
那么我们就可以这样表示自然数了:
1 ≡ λsz.s(z) 2 ≡ λsz.s(s(z)) 3 ≡ λsz.s(s(s(z)))
那么suc函数应该怎么定义才能满足以上情况呢?
S ≡ λnsz.s(nsz)
结合S0 ≡ (λnsz.s(nsz))(λsz.z),观察一下这个S函数的定义
我们发现 nsz 求值得到的是n的函数体,本例中就是 z,那么这个表达式求值得到的函数体就是s(z)
而S执行一次后,其参数为sz
也就是 λsz.s(z)
加法
加法可以使用以下函数:
(λxys.xsy)
观察自然数的函数定义,可以得知若有一自然数N,则Nfi 表示以i为参数应用N次f函数。
结合S(N)即是N+1
则2+3可以如此表示:
2S3 ≡ (λsz.s(sz))(λwyx.y(wyx))(λuv.u(u(uv)))
规约之后:
(λwyx.y((wy)x))((λwyx.y((wy)x))(λuv.u(u(uv)))) ≡ SS3
继续规约得 S4
可不就是 5
乘法
乘法可以使用以下函数:
(λxys.x(ys))
x(ys)将会返回一个函数,形同 (λz.ys(...(ys(z)))),其中ys应用次数为x
所以代入参数x,y之后
函数最终规约为
(λsz.s(...(s(z)))) 其中s的应用次数为x*y
,所以得到数字也就是x*y
(根据之前数字的定义,对zero应用多少次suc函数就是对应次数的数字表示)
计算2和2的乘积:
(λxys.x(ys))22
(λs.2(2s))
(λs.2(2s))
(λs.(λsz.s(sz))(2s))
这里不难看出,这里会将2s(即y)拓展2(即x)次
(λs.(λz.2s(2sz)))
(λsz.2s(2sz))
通过观察,2sz就是s(sz)也就是2,那么上式可以规约为
(λsz.2s2)
继续规约得到的结果也就是4
条件
为了实现条件,我们需要定义"true"和"false":
T ≡ λxy.x
F ≡ λxy.y
观察一下这两个函数的定义,两者都接受两个参数,T返回第一个参数,F返回第二个参数。
逻辑运算
结合T和F的函数定义来看以下函数
∧ ≡ λxy.xy(λuv.v) ≡ λxy.xyF
当且仅当x和y都为T时,结果为T,否则得到结果为F。
∨ ≡ λxy.x(λuv.u)y ≡ λxy.xTy
x和y中至少有个为T时,结果为T,否则得到结果为F。
¬ ≡ λx.x(λuv.v)(λab.a) ≡ λx.xFT
取反。
条件测试
定义一个函数,当且仅当其参数为0时返回T,否则返回F:
Z ≡ λn.nF¬F
这里不能将¬F直接替换为T,因为lambda演算中函数应用是左结合的。
这里的n是数字,还记得数字是怎么定义的吗?
nF¬即是以¬为参数应用n次F函数,而F函数应用至任何参数,其结果都是I(也就是前面提到的 λx.x)。
所以有:
Z0 ≡ (λn.nF¬F)0 = 0F¬F = ¬F = T
ZN ≡ (λn.nF¬F)N = NF¬F = IF = F
predecessor函数
也就是前驱函数,取数字的前一个,与前面的suc函数相反。
这里我们用一个序对(pair)来表示 (n,n-1),那么前驱函数就是取出序对的第二个元素作为结果。
在lambda演算中,序对可以用函数(λf.fab)
来表示,f来指示取序对的哪个元素。
我们之前定义的T和F就可以在这里接着使用,前者用来取出序对的第一个元素,后者则用来取出第二个元素。
函数Φ ≡ (λpf.f(S(pT))(pT))
用来生成序对(n,n-1)
我们可以通过对序对(λz.z00)
应用n次函数Φ然后取其第二个元素来得到数字n的前驱数(也就是n-1)。
P ≡ (λn.nΦ(λz.z00)F
从这里也可以看出,0的前驱数是0。
相等性与不等性
有了之前定义的前驱函数,我们现在就可以测试一个数字x是否大于等于y:
G ≡ (λxy.Z(xPy))
如果对y应用x次P函数得到0则x ≥ y
若x ≥ y且y ≥ x,则x=y,所以我们可以定义一个函数E来判断两数是否相等:
E ≡ (λxy. ∧ (Z(xPy))(Z(yPx)))
类似的,我们可以定义函数来测试x > y, x < y 或是 x != y
递归
lambda演算中如何定义递归函数?
使用Y组合子。
Y ≡ (λy.(λx.y(xx))(λx.y(xx)))
假设我们的递归函数为R,则有
YR = (λx.R(xx))(λx.R(xx))
规约之后
R((λx.R(xx))(λx.R(xx))))
这意味着 YR = R(YR)
支持惰性求值的语言才能使用这一特性,对于急切求值的语言,其会尝试先对函数的参数求值,在以上函数应用中会导致R永远不会被调用并且程序不会终止。
有空接着研究啦~