文章目录
Lambda calculus我们一般称为λ演算,最早是由邱奇(Alonzo Church,图灵的博导)在20世纪30年代引入,当时的背景是解决函数可计算的本质性问题,初期λ演算成功的解决了在可计算理论中的判定性问题,后来根据Church–Turing thesis,证明了λ演算与图灵机是等价的。 好了,经过上边简单的介绍,大家应该对λ演算有了初步印象。下面我将重点介绍λ演算的具体内容,并且阐述λ演算是如何奠基了我们现在常用的编程语言(如:Java、python、Lisp等)。 λ演算的语法与求值语法(syntax)因为λ演算研究的是函数的本质性问题,所以形式极其简单: E = x variables
| λx. E function creation(abstraction)
| E1 E2 function application
上面的E称为λ-表达式(expressions)或λ-terms,它的值有三种形式:
λ表达式例子上面就是λ演算的语法了,很是简单吧。下面看几个例子:
在使用λ演算时,有一些惯例需要说一下:
Currying带有多个参数的函数从上面我们知道,λ演算中函数只有一个参数,那两个参数的函数的是不是就没法表示了呢,那λ演算的功能也太弱了吧,这就是λ的神奇之处,函数在本质上只需要一个参数即可。如果想要声明多个参数的函数,通过currying技术即可。下面来说说currying。
如果用函数式语言clojure来表示就是:
求值(evaluation)在λ演算中,有两条求值规则:
Alpha equivalence这个比较简单也好理解,就是说λx.x与λy.y是等价的,并不因为换了变量名而改变函数的意义。 Beta reduction这个规则是λ演算中函数应用的重点了。一句话来解释就是,把参数应用到函数体中。举一个例子: 求值顺序考虑下面这个函数应用 (λ y. (λ x. x) y) E
有两种计算方法,如下图
值得一提的是,Call by Name这种方式在我们目前的语言中,只有函数式语言支持。 λ演算与编程语言的关系在λ演算中只有函数(变量依附于函数而有意义),如果要用纯λ演算来实现一门编程语言的话,我们还需要一些数据类型,比如boolean、number、list等,那怎么办呢? 听上去很些云里雾里,但看了我下面的讲解以后,你会发现,编程语言原来还可以这么玩,希望我能把这部分讲清楚些,个人感觉这些东西太funny了 :-) 好了,我们先从最简单——boolean的开始。 编码BooleanAsk:我们能对boolean值做什么? 好,知道了能对boolean的操作,下面就用λ表达式来定义它: true = λx. λy. x
false = λx. λy. y
if E1 then E2 else E3 = E1 E2 E3
来简单解释一下,boolean就是这么一个函数,它有两个参数(通过currying实现),返回其中一个。下面看个例子: if true then u else v 可以写成
(λx. λy. x) u v ---> (λy. u) v ---> u
哈哈,很神奇吧,更精彩的还在后头呢,继续 编码pair这里简单解释下pair,其实就是序列对,如(1 2)、(hello world),这些就是pair,只有两个元素,但不要小看了pair,我们用的list就是通过pair连接起来形成的。 Ask:我们能对pair做什么? 好,知道了能对pair的操作,下面就用λ表达式来定义它: mkpair x y = λb. (b x y)
fst p = p true
snd p = p false
这里用到了true与false的编码。解释一下: fst (mkpair x y)--->(mkpair x y) true--->true x y--->x
这样我们就能取到pair的第一个元素了。很好玩吧,下面的更有趣,继续 编码number这里讲的number是指的自然数。 Ask:我们能对number做什么? 好,知道了能对number的操作,下面就用λ表达式来定义它: 0 = λf. λs. s
1 = λf. λs. f s
2 = λf. λs. f (f s)
......
解释一下,利用currying,我们知道上面的定义其实相当于一个具有两个参数的函数:一个函数f,另一个是起始值s,然后不断应用f实现遍历数字的操作。先不要管为什么这么定义,看了下面我们如何定义加法乘法的例子你应该就会豁然开朗了: succ n = λf. λs. f (n f s)
然后,就可以定义加法与乘法了 add n1 n2 = n1 succ n2
mult n1 n2 = n1 (add n2) 0
只看定义要想弄懂应该还是有些困难,下面看个具体的例子: add 0 =
(λn1. λn2. n1 succ n2) 0 --->
λn2. 0 succ n2 =
λn2. (λf. λs. s) succ n2 --->
λn2. n2 =
λx. x
我第一次看这个例子有个疑问,add不是两个参数吗,你怎么就加一个0呢?其实还是currying没理解好,两个参数的函数内部不也是用一个参数的函数来表示的嘛,如果只传递一个参数,那么我们就知道还会返回一个函数,本例中就是 哈哈,看来也不过如此嘛,如果你能看到这里,说明你已经对lambda掌握的差不多了。下面再来看个“难点”的例子—— add 1 1 --->
1 succ 1 --->
succ 1 --->
λf. λs. f (f s) ---> 2
最后一个例子, mult 2 2 --->
2 (add 2) 0 --->
(add 2) ((add 2) 0) --->
2 succ (add 2 0) --->
2 succ (2 succ 0) --->
succ (succ (succ (succ 0))) --->
succ (succ (succ (λf. λs. f (0 f s)))) --->
succ (succ (succ (λf. λs. f s))) --->
succ (succ (λg. λy. g ((λf. λs. f s) g y)))
succ (succ (λg. λy. g (g y))) --->......---> λg. λy. g (g (g (g y))) = 4
不要一看到这么多步骤就吓跑了,原则很简单,就是不断进行函数应用,需要注意的就是这里的2、0不再是单纯的数字了,它是从具有两个参数的函数,如果你应用时只传入一个参数,说明它还会返回一个函数。 不管怎样,如果你已经看到了这里,我希望你能把上面这个乘法的例子看懂,就是不断进行函数应用而已,没什么东西,我觉得难点在于思维的转化,因为以前都很理所当然认为2×2=4了,而不知道这么简单的计算后面的本质性东西,通过这个例子,希望大家能明确一点: 最后再来一个收尾菜: 如果想要判断一个数字是否为0,可以这么定义
iszero n = n (λb. false) true
λ-演算与图灵机本文一开始就说明了,λ-演算与图灵机是等价,这里简单说下我对图灵机的理解: 在一个不限时间、不限资源的前提下,图灵机通过前进、后退、跳转、输出1或0这四个简单的命令,在一条无限长的纸带上执行事先编好的程序。
根据目前的证明,图灵机是宇宙间最强大的机器(理想中的),我们现有的计算机都没有超过图灵机。 如果说一个语言是图灵完备的,就是说,世界上任何可计算性问题,它都能解决。 我们现有的命令式语言,如C、Java等就是以图灵机为基础的。如果说这些语言图灵完备,需要具有以下两个特征:
与图灵机对应,λ-演算的直接影响是函数式编程语言,如lisp、Haskell等,如果说这些函数式语言图灵完备,需要有以下两个特征:
鉴别一个语言是不是函数式的标准是:这个语言能否在运行时创建函数,如果能,就是函数式语言。 总结通过上面长篇大论(希望你能抽时间看完),我们用一些无意义符号表达了我们已经熟知的一些概念,这就是λ演算的精髓之处,通过一套形式化的规则来描述这些东西,要知道,这里面的很多东西我们现如今想当然的接受了,但如何让笨重的计算机来理解这个世界呢,这就需要这些形式化的规则来指导了。 我这里介绍的lambda calculus并不完全,只是其中的一部分,像递归这个重要的东西就没说,大家凭借兴趣再自己去看吧,我觉得我这篇文章就是个砖头,希望能引出大家的宝玉就好。 我们现在的编程语言趋向于多范式化,像python、ruby的兴起就说明了这点。 有了对 lambda 的认识后,就可以尝试下一个主题了——Y 算子,下面给出我觉得讲解的最好的一篇文章: 最后,推荐王垠的一篇文章,以飨读者: |
|