Lambda
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Lambda演算
Lambda演算是一个形式系统,它被设计出来用来研究函数定义,函数应用和递归。
它是在二十世纪三十年代由Alonzo Church 和 Stephen Cole Kleene发明的。
Church在1936年使用lambda演算来证明了判定问题是没有答案的。
Lambda演算可以用来清晰的定义什么是一个可计算的函数。
两个lambda演算表达式是否相等的问题不能够被一个通用的算法解决,这是第一个问题,它甚至排在停机问题之前。
为了证明停机问题是没有答案的,不可判定性能够被证明。
Lambda演算对于函数式编程语言(例如lisp)有重大的影响。
同时,数理逻辑中对于lambda演算的介绍就简单得多:
λ-演算可以说是最简单、最小的一个形式系统。
它是在二十世纪三十年代由Alonzo Church 和Stephen Cole Kleene发明的。
至今,在欧洲得到了广泛的发展。
可以说,欧洲的计算机科学是从λ-演算开始的,而现在仍然是欧洲计算机科学的基础,首先它是函数式程序理论的基础,而后,在λ-演算的基础上,发展起来的π-演算、χ-演算,成为近年来的并发程序的理论工具之一,许多经典的并发程序模型就是以π-演算为框架的。
这里不由得想起一位我尊敬的老师的博士毕业论文就是关于π-演算的,可惜这位老师已经去别的学校了。
Lambda演算表达了两个计算机计算中最基本的概念“代入”和“置换”。
“代入”我们一般理解为函数调用,或者是用实参代替函数中的形参;“置换”我们一般理解为变量换名规则。
后面会讲到,“代入”就是用lambda演算中的β-归约概念。
而“替换”就是lambda演算中的α-变换。
Lambda演算系统的形式化定义
维基百科全书上面的对于lambda演算的定义不是很正规,但是说明性的文字较多。
而数理逻辑中的定义很严密,不过没有说明不容易理解。
我尽可能把所有资料结合起来说明lambda演算系统的定义。
字母表
lambda演算系统中合法的字符如下:
1. x1,x2,x3,…变元(变元的数量是无穷的,不能在有限步骤内穷举,这个很重要,后面有定理是根据这一点证明的)
2. à 归约
3. =等价
4. λ,(,)(辅助工具符号,一共有三个,λ和左括号右括号)
所有能够在lambda演算系统中出现的合法符号只有以上四种,其他符号都是非法的。
例如λx.x+2,如果没有其他对于+符号的说明,那么这就是一个非法的λ演算表达式。
λ-项
λ-项在一些文章中也称为λ表达式(lambda expression),它是由上面字母表中的合法字符组成的表达式,合法的表达式组成规则如下:
1. 任一个变元是一个项
2. 若M,N是项,则(MN)也是一个项(function application,函数应用)
3. 若M是一个项,而x是一个变元,则(λx.M)也是一个项(function abstraction,函数抽象)
4. 仅仅由以上规则归纳定义得到的符号串是项
说明1:λ-项是左结合的,意思就是若f x y都是λ-项,那么f x y=(f x) y
说明2:(λx.M)这样的λ-项被称为函数抽象,原因是它常常就是一个函数的定义,函数的参数就是变量x,函数体就是M,而函数名称则是匿名的。
用一个不恰当的比喻来说,我们通常认为的函数
f(x)=x+2,可以被表达为λx.x+2。
因为+是未定义的,所以这个比喻只是为了方便理解而已。
说明3:MN这样的λ-项被称为函数应用,原因是它表达了将M这个函数应用到N这个概念。
沿用上面的例子f(x)=x+2,那么f(2)=2+2;同样的λx.x+2表达了f(x)的概念,那么(λx.x+2)2表达了f(2)的概念。
其中M=λx.x+2,N=2,所以MN=(λx.x+2)2。
说明4:注意说明3只是为了方便理解,但是还存在很多与直观理解不符合的地方。
例如xy也是一个合法的λ-项,它们也是MN形式的,不过x和y都仅仅是一个变量而已,谈不上函数代入。
上面是λ-项的形式化定义,有一些是可以与函数理论直观联系的,而另一些只是说明这个λ-项是合法的,不一定有直观的字面意义。
公式
若M,N是λ-项,则MàN,M=N是公式。
λ-项中的变量自由出现法则
在一个λ-项中,变量要么是自由出现的,要么是被一个λ符号绑定的。
还是以函数的方式来理解变量的自由出现和绑定。
例如f(x)=xy这个函数,我们知道x是和函数f相关的,因为它是f的形参,而y则是和f无关的。
那么在λx.xy这个λ-项中,x就是被λ绑定的,而y则是自由出现的变量。
直观的理解,被绑定的变量就是作为某个函数形参的变量,而自由变量则是不作为任何函数形参的变量。
Lambda变量绑定规则:
1. 在表达式x中,如果x本身就是一个变量,那么x就是一个单独的自由出现。
2. 在表达式λ x. E中,自由出现就是E中所有的除了x的自由出现。
这种情况下在E中所有x的出现都称为被表达式中x前面的那个λ所绑定。
3. 在表达式(MN )中,变量的自由出现就是M和N中所有变量的自由出现。
另一种关于变量的自由出现的规则也许更直接:
1. free(x) = x
2. free(MN) = free(M) È free(N)
3. free(lx • M) = free(M) – {x}
为什么要花大力气来给出变量自由出现的规则,是因为后面的很多地方要用到变量的自由出现的概念。
例如α-变换和β-归约。
例子:分析λf.λx.fx中变量的自由出现和绑定状况。
λf.λx.fx =λf.E, E=λx.fx
E=λx.A, A=A1A2, A1=f, A2=x
所以在A中f和x都是自由出现的,
所以E中x是绑定λ x
所以整个公式中f是绑定第一个λ f的。
λ x的控制域
来看两个λ-项,λx.xx和λx.(xx)有何不同?根据左结合的法则,λx.xx=(λx.x)x,其中第一个x是被λ绑定的,而第二个x则是自由出现的。
而λx.(xx)中两个x都是被λ绑定的。
这表明了两个λ-项中λx的控制域是不同的。
我们知道谓词演算中量词也是有控制域的,λx的控制域与它们类似,这里就不给出详细的定义了。
其实也很直观。
α-变换(α-conversion)
α-变换规则试图解释这样一个概念,λ演算中约束变量的名称是不重要的,例如λx.x和λy.y是
相同的函数。
因此,将某个函数中的所有约束变量全部换名是可以的。
但是,换名需要遵循一些约束。
首先是一个说明:如果M,N是λ-项,x在M中有自由出现,若以N置换M中所有x的自由出现(M 中可能含有x的约束出现),我们得到另一个λ-项,记为M[x/N]。
α-变换规则如下:
λx.M=λy.M[x/y] 如果y没有在M中自由出现,并且只要y替换M中的x,都不会被M中的一个λ绑定。
例子:λx.( λx.x)x = λy(λx.x)y
α-变换主要用来表达函数中的变量换名规则,需要注意的是被换名的只能是M(函数体)中变量的自由出现。
β-归约
β-归约表达的是函数应用或者函数代入的概念。
前面提到MN是合法的λ-项,那么MN的含义是将M 应用到N,通俗的说是将N作为实参代替M中的约束变量,也就是形参。
β-归约的规则如下:
(λx.M)N à M[x/N] 如果N中所有变量的自由出现都在M[x/N]中保持自由出现
β-归约是λ演算中最重要的概念和规则,它是函数代入这个概念的形式化表示。
一些例子如下:
(lx.ly.y x)(lz.u) ® ly.y(lz.u)
(lx. x x)(lz.u) ® (lz.u) (lz.u)
(ly.y a)((lx. x)(lz.(lu.u) z)) ® (ly.y a)(lz.(lu.u) z)
(ly.y a)((lx. x)(lz.(lu.u) z)) ® (ly.y a)((lx. x)(lz. z))
(ly.y a)((lx. x)(lz.(lu.u) z)) ® ((lx. x)(lz.(lu.u) z)) a
需要多加练习才能习惯这种归约。
η-变换(η-conversion)
η-变换表达了“外延性”(extensionality)的概念,在这种上下文中,两个函数被认为是相等的“当且仅当”对于所有的参数,它们都给出同样的结果。
我理解为,对于所有的实参,通过β-归约都能得到同样的λ-项,或者使用α-变换进行换名后得到同样的λ-项。
例如λx.fx与f相等,如果x没有在f中自由出现。
λ演算的公理和规则组成
这一段来自《数理逻辑与集合论》(第二版清华大学出版社)。
还修正了其中的一个错误。
1. (λx.M)N à M[x/N] 如果N中所有变量的自由出现都在M[x/N]中保持自由出现
2. MàM
3. MàN, NàL => MàL (原书中此处错误)
4. MàM’=>ZMàZM’
5. MàM’=>MZàM’Z
6. MàM’=>λx. M àλx. M’
7. MàM’=>M=M’
8. M=M’=>M’=M
9. M=N,N=L=>M=L
10. M=M’ => ZM = ZM’
11. M=M’ => MZ = M’Z
12. M=M’ =>λx. M =λx. M’
如果某一公式MàN或者M=N可以用以上的公理推出,则记为λ├MàN和λ├M=N。
范式
如果一个λ-项M中不含有任何形为((λx.N1)N2)的子项,则称M是一个范式,简记为n.f.。
如果一个λ-项M通过有穷步β-归约后,得到一个范式,则称M有n.f.,没有n.f.的λ-项称为n.n.f.。
通俗的说法是,将一个λ-项进行β-归约,也就是进行实参代入形参的过程,如果通过有穷步代入,可以得到一个不能够再进行代入的λ-项,那么这就是它的范式。
如果无论怎样代入,总存在可以继续代入的子项,那么它就没有范式。
例子
M = λx.(x((λy.y)x))y,则Mà y((λy.y)y) à yy。
M有一个n.f.。
例子
M =λx.(xx) λx.(xx),则Màλx.(xx) λx.(xx)=M。
注意到M的归约只有唯一的一个可能路径,所以M不可能归约到n.f.。
所以M是n.n.f.。
注意这个λx.(xx) λx.(xx)在λ演算的协调性研究中是一个比较经典的项。
((λ x. x x) (λ x. x x))被称为Ω, ((λ x. x x x) (λ x. x x x))被称为Ω2。
不动点理论
Λ表示所有的λ-项组成的集合。
定理:对于每一个F∈Λ,存在M∈Λ,使得λ├FM=M。
证明:定义w=λx.F(xx),又令M=ww,则有λ├M=ww=(λx.F(xx))w=F(ww)=FM。
证明是非常巧妙的,对于每个F,构造出的这个ww刚好是可以通过一次β-归约得到F(ww)=FM,如果再归约一次就得到F(FM),这样可以无限次的归约下去得到F(F(F(F…(FM)…)))。
Church-Rosser定理及其等价定理
这两个定理告诉我们这样一个事实:如果M有一个n.f.,则这个n.f.是唯一的,任何β-归约的路径都将终止,并且终止到这个n.f.。
Church-Rosser定理:如果λ├M=N,则对某一个Z,λ├MàZ并且λ├NàZ。
与之等价的定理如下,
Diamond Property定理:如果MàN1,MàN2,则存在某一Z,使得N1àZ,N2àZ。
后记
最后两个定理的证明没有怎么看懂,所以没有在笔记中记下。
另外,前天在网上订购的《程序设计语言:概念和结构》第二版刚刚拿到手,其中有关于λ演算的一章。
应该也对我大有帮助,待看完再说。
学习λ演算的初衷是了解一些程序设计的最基本原理,没想到最后还是绕到形式系统和数理逻辑这儿来了。
不过,形式化表达的λ演算更清晰。
国内大学虽然也开程序设计的课程,不过好像都是pascal,c/c++之类,关于程序设计的本质基础的程序好像并不多。
随着大学扩招,中国有望在很短的时间内成为世界上程序员最多的国家,如果仅仅只学命令式和面向对象的程序设计,一定是不够的。
函数式和逻辑式程序设计语言也是要学学的啊。
文中肯定有很多错漏,希望看出来的人给我留言。
参考文献
School of Computer Science and Software Engineering, Faculty of Information Technology, Monash University, Australia 3800
这个大学的FP课程中的Lambda演算相关部分
.au/~lloyd/tildeFP/Lambda/Ch/
wikipedia中lambda演算相关介绍
/wiki/Lambda_calculus
《数理逻辑与集合论》第二版清华大学出版社
Lambda算子
Currying
据说Currying翻译为局部套用函数,也不知真假。
喜欢吃印度美食的老大们不要激动。
Currying 和咖喱没有半点关系。
这个技巧以逻辑学家Haskell Curry的姓命名。
Haskell Curry也是名动一时的人物。
他和Moses Schönfinkel共创了组合逻辑(combinatory logic),并把这们学科发扬光大。
当初Curry搞出组合逻辑,主要是为了在数理逻辑里避免使用变量。
后来搞函数编程的人们发现,组合逻辑是一类函数编程语言的理论基础。
一些函数语言里常见的特性,比如说高阶函数合lazy evaluation, 就是用组合逻辑里的combinator实现的。
当初Alanzo Church对这个理论也相当熟悉。
难说lambda理论不是受了组合逻辑的影响。
大牛Philip Wadler为了纪念Curry, 把他的函数语言叫做Haskell。
Haskell也是一门巨酷的函数语言,兼顾数学的优美和软件开发的实用性。
连LInspire的开发组都决定用Haskell作为系统开发的语言(但我很奇怪他们为什么放弃使用另一门酷酷的函数语言Ocaml)。
说远了。
解决参数限制的关键在于认识到函数也是数据(用更严格的说法,是值)。
既然是数据,就可以传来传去。
如果有两个参数,我们可以写一个接受第一个参数的函数,而这个函数返回的是接受第二个参数的函数。
“就那么简单!我们在JavaScript里不是常用这个功能?” 嘻嘻,我们在JavaScript里的确常用这个功能。
JavaScript其实是带C句法的函数语言,支持高阶函数,自然支持Currying。
JavaScript的功能其实颇为强大,不然Douglas Crockford不会说JavaScript是最被人误解的语言。
举例来说,假设我们要写一个函数,把x和y相加。
最自然的写法是lambda x y . plus x y. 既然我们只能一次接受一个参数,我们可以先写一个接受x 的函数。
这个函数返回一个接受y 的函数。
这个被返回的函数把x 和y 相加:lambda x.(lambda y. plus x y)。
简单吧?数学奇妙之处就在于我们用极为简单的砖块搭建出恢弘的宫殿。
事实上,数学家们总是极力追求理论基础的简洁。
他们不知疲倦地挥舞着奥卡姆剃刀,直到把自己的理论切割成东家之子:增之一分则太长,减之一分则太短。
有了Currying这个工具,我们可以放心使用多参数的标记了。
反正多参数的lambda不过是单参数lambda的方便用法而已,没有任何实质上的改变。
自由vs 有界标识符
标识符和变量其实是一个意思。
我记得国内教材里很少用标识符这个说法。
不过既然原作者用这个说法,我就跟着用了。
上次说到Currying解决了如何处理多参数的问题。
在讨论怎么使用lambda前,我们还要解决一个细微但重要的语法问题:封闭(closure),或者叫完全有界(complete bounding)。
这里的有界和一阶谓词逻辑里的有界没有本质区别,对一阶谓词逻辑熟悉的老大们可以放心跳过。
其实有界涉及的定义很直观,我们看一个例子先。
假设我们有一个函数lambda x y. (lambda y. y + 2) + x + y +z,lambda y. y+2里的y和它后面的y是不是一样的呢?显然它们是不一样的。
为了处理这种区别,我们引入了有界。
当一个lambda表达式被计算时,它不能处理无界的标识符。
当一个标识符出现在一个lambda表达式的参数里,并且被包含在这个lambda表达式里,我们就可
以说这个标识符有界。
如果一个标识符没有被包含在任何一个表达式里,我们就叫它为自由变量。
比如说,上面那个lambda表达式里,x 出现在lambda x y .(....)里,所以它是有界的变量,它的包含环境(enclosing context,用“语境”或者“上下文”怎么听怎么别扭,好像俺是《读书》的御用作者似的。
:-D)是整个lambda表达式。
lambda y. y+2里的y也是有界的,但它的包含环境是lambda y. y+2。
标识符z没有出现在包含它的表达式的参数列表里,所以是自由变量。
再举几个例子:
mbda x . plus x y: 这个表达式里,"y"和"plus"都是自由变量,因为它们不是任何
包含它们的表达式的参数。
x有界,因为它被包含在plus x y里,而plus x y的参数有x。
mbda x y.y x: 这个表达式里,x和y都有界,因为它们是这个表达式的参数。
mbda y . (lambda x . plus x y): 在内嵌的表达式lambda x. plus x y里,y
和plus 是自由变量而x是有界变量。
在整个lambda表达式里,x和y都有界:x在内嵌表达式界内,而y在整个表达式界内。
plus仍然自由。
我们用"free(x)"来代表表达式x里所有自由变量的集合。
一个lambda表达式完全合法仅当它的所有变量都有界。
不过当我们考查某个复杂表达式里的子表达式且不考虑上下文时,那些子表达式可以有自由变量-其实确保正确处理那些子表达式里的自由变量非常重要。
Lambda 算子计算规则
其实真正的规则就俩:alpha和beta。
Alpha规则又叫转换(conversion)规则,而beta规则又叫简化(reduction)规则。
Alpha转换
这个充满了《星际迷航》味道的规则其实就是重命名操作。
它无非是说变量名不重要:给定任何一个lambda表达式,我们可以任意改变参数的名字,只要我们相应地改变这些对应这些参数的变量名字。
比如说,我们有如下表达式:
lambda x . if (= x 0) then 1 else x^2
我们通过alpha规则把X改成Y(写作alpha[x/y], 和逻辑里的变量替换一个写法),于是得到:lambda y . if (= y 0) then 1 else y^2
Alpha操作完全不影响lambda表达式的意义。
不过我们后面会发现,这个操作很重要,因为它让我们能够实现诸如递归的操作。
Beta简化
Beta简化就有意思了。
我们只需要这一个规则,就可以让lamdba算子实现一台计算机能做的任何计算。
透过纷繁的表象,我们会发现事情的本质往往出人意料地清晰而简单。
删繁为简,恰是数学魅力所在。
Beta规则无非是说,应用一个函数(也就是lambda表达式。
一个意思)等价于通过把函数体内有界的变量替换成应用里对应参数的实际值来替换原来的函数。
听上去有些拗口(呵呵,其实原文更拗口),但当你看一个例子就知道它其实很简单:
假设我们要应用一个函数:"(lambda x . x + 1) 3"。
Beta规则说,我们可以替换整个表达式,把函数体(也就是“x+1”)里的参数对应的x替换成实际的值3。
所以最后的结果是“3+1”。
再来一个稍微复杂点的例子:
lambda y . (lambda x . x + y)) q
这个表达式有意思,因为应用了这个表达式后,我们可以得到另外一个表达式。
也就是说,它是一个生成表达式的表达式(说到这里,玩儿动态语言的老大们可以笑了,玩儿C/C++/Java的老大们可以流口水了)。
当我们对这个表达式应用Beta简化时,我们把所有对应参数y的变量替换成实际的值q。
所以结果是"lambda x, x+q"。
再来一个例子:
"(lambda x y. x y) (lambda z . z * z) 3". 这个带两个参数的函数把第一个参数应用到第二个参数上。
当我们计算它的值时,我们把第一个lambda表达式里的变量x换成lambda z. z * z, 再把变量y换成3,得到(lambda z. z * z) 3。
对该结果应用Beta简化,我们得到3 * 3。
Beta的严格定义如下:
lambda x . B e = B[x := e] if free(e) \subset free(B[x := e]
这个定义末尾的条件,"if free(e) \subset free(B[x:=e])"道出了我们需要Alpha转换的原因:仅当beta化简不会引起有界变量和自由变量的冲突时,我们可以实施Beta化简。
如果一个变量“z”是"e"里的自由变量,那我们得保证beta化简不会让"z"变成有界变量。
如果B里的有界变量和”e"里的自由变量重名,我们必须先用Alpha转换,是的重名的变量不再重名。
形式化定义不够直观,直观描述又不够简洁。
还是来个例子漱漱口:
给定一个表达式,lambda z. lambda x. x+z. 假设我们要应用这个表达式:
(lambda z . (lambda x . x + z)) (x + 2)
在实际参数"(x + 2)"里,x是自由变量。
但x不是表达式lambda x. x+z的自由变量。
也就是说,free(e) \subset free(B[ x:=e])不成立。
如果我们打破Beta简化的规则,直接开始Beta简化,便会得到: lambda x . x + x + 2
"x+2"里自由变量,x,现在变得有界了。
如果我们把结果应用到3上:(lambda x. x+2+2) 3,我们得到3 + 3 + 2。
如果我们按正常程序办事呢?
应用alpha[x/y]: lambda z . (lambda y . y+z)) (x + 2)
应用beta: lambda y . y + x + 2) 3
再次应用beta: 3 + x + 2.
"3+x+2" 和"3+3+2" 很不一样哈!
规则就这些了。
我们还可以选择性地加一个所谓的Eta-化简,不过它不是必需的。
我们就此跳过。
我们讨论的这套系统已经是图灵完备的计算体系。
那这套系统到底有什么用嗫?到底怎样才能让这套系统变得真正有用嗫?嗯,要说明这些问题,我们得先定义一些基本的函数,以便我们做算术,条件测试,递归,等等。
这些会在以后的帖子里谈到。
我们也还没有谈到适合lambda算子的模型(Good Math Bad Math的作者在这里和这里讨论了模型)。
模型也是很重要的东西。
逻辑学家用了好几年时间研究lambda算子,才搞出一个完备的模型。
而且早先时候,尽管lambda算子看起来没错,为它制订模型的工作却失败了。
这在当时极为引人关注。
要知道,毕竟一个系统没有有效的模型就没有实际的意义。
阿隆佐.丘齐(Alonzo Church)的天才:Lambda算子里的数
前面建立了lambda运算的基本规则,就可以用lambda算子做点有意思的东西了。
开始前为方便计,我们先来点语法糖花差花差,用来命名函数。
这些语法糖可以让复杂的公式好写一点。
我们用"let" 来引入一个“全局”函数(也就是说,我们用这个函数时,不用在每个表达式里定义一次):let squer = lambda x. x^2
这个式子申明了一个叫"square"的函数,定义为lamdba x. x^2。
如果我们有一个表达式"square 4",上面的"let"意味着这个表达式和下面这个表达式一样:
(lambda square. square 4)(lambda x. x^2)。
这个"let"是从Common Lisp或者Scheme里借来的。
Lambda算子里可没有这个东西。
数学家推崇“如无必要,毋增实体”。
这些关键字不入他们的法眼。
不过对写惯了程序的我们来说,这些句法糖就可爱多了。
我们的例子里会用到数字和算术操作符。
不过记住lambda算子里根本没有数字。
我们只有函数!所以我们需要发明用函数来创造数字的方法。
幸好Alonzo Church是个天才。
他既然发明了lambda 算子,用lambda算子表征数字自然不在话下。
他搞出的用于数字的函数自然就叫做丘齐数(Church Numerals)。
丘齐数里,所有的数字都是两个参数的函数:
1.零是lambda s z . z
2.一是lambda s z . s z
3.二是lambda s z . s (s z)
4.对任意一个数"n",它的丘齐数都是一个函数。
这个函数把它的第一个参数应用到第二个参
数上n次。
用流行的写法,就是lambda s z . s s n z。
绕口啊绕口。
做形式化的东东不幸之处就是成天和绕口令打交道。
解脱这道呢?当然就是牢记牛人费因曼在Connection Machine工作时的学习方法:问最简单的问题。
“给我最简单的例子”。
“怎么才能验证这是正
确的?”。
比如说零(lambda s z . z)吧,第一个参数是s, 应用零次就是没有,所以函数体就是孤零零的"z"。
那数字一呢?当让就是把第一个参数,s,应用到z上一次,所以函数体就变成了"s z"。
理解这个定义的方法之一时把"z"看作丘齐数里零的名字,而把"s"看后继函数(successor function)的名字。
“后继函数”其实很简单,C/C++里的++是也。
所以呢,零就是一个返回"0"这个值的函数;一就是把后继函数应用到零上一次的函数;二就是把后继函数应用到一上一次或者说零上两次的函数。
0++ 得到1, 1++ 等价与(0++)++,而1++得到2.现在把0换成z,把++换成s, 一切就清楚了。
现在--看好了。
如果我们想做加法,x+y,我们需要一个带4个参数的函数。
两个参数代表相加的两个数字,以及为得到结果而需要的"s"和"z"。
let add = lambda s z x y . x s (y s z)
看着好像有点不知所云。
不过我们可以用Curry这个利器,分开"s" "z"和x, y。
首先,Curry后得到的函数带两个参数,x和y(这个好比add(x, y),符合我们对加号的理解)。
其次,我们需要正规化x和y需要的s和z,让x和y共享相同的零和后继函数的绑定:
let add = lambda x y. (lambda s z . (x s (y s z)))
仔细观察一下,上面的式子无非是说,要把x和y相加,我们先用"s"和"z"创建丘齐数"y",然后在把"x"应用到y上。
应用时需要的"s"和"z"是"y"里的"s"和"z"。
也就是说,我们的到的结果是一个函数,这个函数把自己加到另一个函数上。
还是用例子来说明问题。
比如说2+3:
add (lambda s z. s (s z)) (lambda s z . s (s (s z))) news newz
为了让演算变得稍微容易一点,我们先对2和3来个Alpha转换。
让2用s2和z2,而3用s3和z3:
add (lambda s2 z2 . s2 (s2 z2)) (lambda s3 z3 . s3 (s3 (s3 z3)))
现在我们可以把"add"替换成它的定义了:
(lambda x y .(lambda s z. (x s y (s z)))) (lambda s2 z2 . s2 (s2 z2)) (lambda s3 z3 . s3 (s3 (s3 z3)))
现在可以对"add"用beta变换了(温馨提示:也就是把形参x和y换成对应的实参):
lambda s z . (lambda s2 z2 . s2 (s2 z2)) s (lambda s3 z3 . s3 (s3 (s3 z3)) s z)
然后我们可以对3这个丘齐数做beta转换。
这步操作其实是“正规化”3:把3的定义里的后继函数和零函数(还记得零是个函数吧?)替换成add的参数列表里的后继函数和零函数:
lambda s z . (lambda s2 z2 . s2 (s2 z2)) s (s (s (s z)))
嗯,有点眉目了。
现在是真正漂亮的地方了。
再来次对2的Beta变换。
看看我们准备做什么:2是个带两个参数的函数:一个参数是后继函数,另一个是零函数。
要把2加到3上,我们只需要用到"add"这个函数的后继函数。
也就是说,我们把计算了3后的结果当成零函数的值!
lambda s z . s (s (s (s (s z)))
而这个式子,正是丘齐数5!
丘齐数酷的地方是它抛弃了传统整数的概念,用函数取而代之。
它把每个数对应为一个函数。
而数数(counting)这个操作被对应为应用某个函数(在这里是后继函数)的次数。
当然了,上面的介绍非常简单。
对丘齐数感兴趣的,可以看这篇文章。
丘齐数对编程有什么用嗫?俺还真不知道。
但丘齐数(进而到丘齐编码)确实一系列基础理论中有重要应用,比如说有类型的lambda算子。
不过这点重要吗?不重要吗?重要吗?不重要吗?研究研究嘛。
Lambda算子里的布尔值和选择
原文在这里。
既然Lambda算子里有了数的概念,我们想进行任意的计算就只需要两件东西了:怎么表示选择,和怎么表达重复操作。
我们先聊聊怎么表示布尔值(也就是非真即假的二元集合)和选择,然后再讨论重复和递归(友情预告:人见人爱的Y Combinator终于可以出场了)。
我们一般把选择表示为if/then/else的表达式,和大多数编程语言的选择语句没有区别。
丘齐数的基本模式无非是把一个数表达为一个函数。
这个函数把它自己加到另外一个函数上。
我们继续沿用这个模式,把true和false也表达为对自己的参数执行if-then-else操作的函数:
let TRUE = lambda x y . x
let FALSE = lambda x y . y
现在我们就可以写“if-then-else”函数了(记到哈,lambda算子理论里所有东东都是函数)。
这个函数的第一个参数是一个条件表达式,第二个参数是当第一个参数为真时返回的表达式,而第三个参数自然是当第一个参数为假时返回的表达式了。
相当于我们的if cond then true_expr else false_expr:
let IfThenElse = lambda cond true_expr false_expr . cond true_expr false_expr
为了我们刚定义的布尔值有用,我们还得定义一些常用的逻辑操作先:
let BoolAnd = lambda x y . x y FALSE
let BoolOr = lambda x y. x TRUE y
let BoolNot = lambda x . x FALSE TRUE
上面定义了常用的“与”,“或”,和“非”操作。
我们可以稍微考查一下它们的机制。
BoolAnd TRUE FALSE (也就是 true && false):
我们把TRUE和FALSE替换为它们的定义: BoolAnd (lambda x y . x) (lambda x y . y) 执行Alpha 替换避免混淆变量名:BoolAnd (lambda xt yt . xt) (lambda xf yf . yf) 然后把BoolAnd替换为它的定义:(lambda x y . x y FALSE)(lambda xt yt . xt) (lambda xf yf . yf)
执行Beta替换:(lambda xt yt . xt) (lambda xf yf . yf) FALSE
呵呵,再Beta一把:(lambda xf yf . yf)。