前言
在Lambda演算中,函数是没有名字的(都是匿名函数),那么如果函数没有名字,也就无法在函数体内显式地调用自身,也就无法定义递归函数,Y combinator就是用来解决这个问题的。
这篇文章想抛开那些数学概念,用程序语言(Scheme)的形式来讲解我们是如何推导出Y combinator的。
运行环境:
IDE:DrRacket
什么是递归函数?
我们以递归函数length
为例,length
函数接受一个list,返回list的元素数目。
1 | (define length |
若你熟悉Scheme函数定义,可以跳过本小结后面部分。
(define length
,define
表达式将一个表达式(表达式即可能是函数也可能是值,在Scheme中并不区分这两点)绑定至一个名字,形式如同(define name func)
,之后便可以使用name
调用func
这个函数。在本定义中,func
是后面整个用括号包围的lambda
表达式,也就是2-4行。
(lambda args body)
,lambda
表达式定义了一个匿名函数,表达式中第二个元素是参数列表,第三个元素是函数体。也就是说,这个匿名函数接受一个参数lst
,然后对函数体进行求值。
(if condition then else)
,if
表达式,若condition
为真,则求值then
,若condition
为假,则求值else
。
null?
接受一个list,判断是否为空。
add1
接受一个值,将其加1后返回,定义如下:
1 | (define add1 |
rest
返回lst
去掉第一个元素的剩余部分,其实就是(define rest cdr)
。
也就是说,length
函数接受一个list,若list为空,则返回0
,若list非空,则求去掉第一个元素的list长度,并将其加一。求值过程如下:
1 | (length `(1 2)) ; `(1 2) is a list has two elements |
假如length没有名字?
如果我们去掉define
,拿出其中的函数定义,是这样。
1 | (lambda (lst) |
**WAIT!!**既然现在没有了length
,显然我们也无法在函数体内调用length
了。
那么我们应该把length
换成什么呢?
我也不知道。
那么我们先试一试换成别的吧。
假设我们现在有一个函数id
,这个函数什么都不做,只接受一个参数并将其返回,定义如下。
1 | (define id |
那么,我们把id
丢进length
定义里面试试:
1 | ; length0 |
看上去是个奇怪的函数,这个函数能工作吗?
我们可以试一试。
1 | > ((lambda (lst) |
我们喂给这个奇怪的函数一个空list,它居然能返回正确的值!
具体原因,请读者先自己思考一下(笑
。
。
。
很简单,因为(null? lst)
为真,所以if
表达式直接返回了0
,压根没有对(add1 (id (rest lst)))
进行求值。
那么我们如果喂(apple)
呢,显然这个奇怪的函数并不能返回正确的长度,因为(id (rest lst))
无法给出“去掉第一个值的list的长度”。
总结一下,这个奇怪的函数:
- 若list长度为0,可以正常工作
- 若list长度大于0,无法工作
那么,我们将这个函数叫做length0
,代表它对长度为0及0以下(并不存在)的list可以正常工作。
length1, length2, and …
好了,我们现在有一个length0
,可以求出空表的长度。
那么下面这个函数有什么作用呢?
1 | ; length1 |
由于(length0 (rest lst))
是可以正确计算的,所以上面这个函数,可以计算长度小于等于1的list的长度。
你可能会问,但是我们没有length0
这个名字呀?
没关系,我们将length0
代换成上一小结的匿名函数形式,如下:
1 | ; length1 |
那么这个函数,我们就可以把它叫作length1
了,因为它可以正确求出长度小于等于1的list的长度!
那么,length2
就再来一次啦,把length1
丢进那个定义,也不需要显式使用length2
啦!
1 | ; length2 |
试一试length2
!
1 | > ((lambda (lst) |
真棒!现在能解决长度小于等于2的list了!
依此类推,我们能定义出length3
、length4
、length5
,甚至length100
、length1000
、length10000
了!
那么如果我们能写出length∞
,不就得到了我们的原始length
函数了吗,因为length
是可以处理任意长度的list的函数!
但是,我们没办法写出length∞
,因为无穷行代码哪里也放不下啊!(笑
试着进行抽象
试着观察length0
length1
length2
,我们会发现,这三个函数都遵循相同的形式,只有last-length
这个地方是需要改变的:
1 | ; same pattern |
根据DRY原则,我们为什么不把相同的形式给抽象出来呢?
我们可以写出一个函数,接受一个参数last-length
,然后就可以利用这个函数生成之后的length[i]
了。
就像这样:
1 | ; abstract-length |
这个函数接受参数last-length
,然后就会返回一个新的函数,可以比last-length
多处理一个长度的list。
那么,length0
可以表示为:
1 | ; length0 |
那么同理,length1
可以表示为:
1 | ; length1 |
当然,我们并没有length0
,需要将其转换为匿名形式,也就是这样:
1 | ; length1 |
那么length2
?
容易,再来一次:
1 | ; length2 |
**WAIT!!!**这难道不是依然在重复增加代码吗?
好吧,其实我们还是有所推进,别急,慢慢来。
返回length函数的函数:make-length
按照我们刚刚的抽象,abstract-length
:
1 | ; abstract-length |
对于这个函数,我们如果:
- 喂给它
id
,即(abstract-length id)
,那么它会返回length0
- 喂给它
length[i]
,即(abstract-length length[i])
,那么它会返回length[i+1]
既然这个函数能够返回我们需要的length[i]
,不妨将其称之为make-length
函数。
那就可以写成函数应用的形式,就像我们对last-length
的抽象:
1 | ; length0 |
依然运用我们的old trick,将abstract-length
写成匿名形式:
1 | ; length0 |
好了,虽然这个形式看上去很复杂,但是它就是我们熟悉的length0
!
那么,length1
呢?
很简单,我们只需要将length0
喂给我们的abstract-length
,就会得到length1
。也就是将length0
中的id
替换成length0
:
1 | ; length1 |
换成匿名表达:
1 | ; length1 |
再进一步,length2
:
1 | ; length2 |
那,length3
?
1 | ; length3 |
好了好了,就不再继续演示了。
那么回顾一下我们的新表示方法,好像它们之间的差别,仅仅在于对参数id
应用了多少次make-length
函数:
length0
: (make-length id)length1
: (make-length (make-length id))length2
: (make-length (make-length (make-length id)))length3
: (make-length (make-length (make-length (make-length id))))
是不是有点类似汉诺塔?
快到终点了
事实上,每当我们使用length
函数,我们并不会给它一个无限长的list。
那么,只要我们用一个足够大的length[i]
,可不可行呢?按照我们上一小节的抽象,我们可以轻易地写出length100
,甚至length10000000000000000
。
问题在于,这个足够大的数,仍然可能不够大。
那么什么时候我们会知道,这个数不够大呢?
显然,当我们需要用到最最最最内层的函数id
,也就是计算(id (rest lst))
的时候,就代表,这个数不够大了。
**注意:**我们喂给length2
一个长度小于等于2的list时,id
的函数应用是没有求值的。
那么,如果每当我们需要对id
的函数应用求值的时候,我们再对其应用一次make-length
,不就可以避免对id
的函数应用求值了吗?
那么,如果我们从不对id
的函数应用求值,也就是说,id
根本没什么用,我们可以把任意函数传递给最内层的make-length
。
那么,为何不试一试把make-length
作为最内层的参数传给make-length
呢。
那么,length0
:
1 | ; length0 |
回过头来看,其实last-length
也是接受一个参数length[i]
,然后返回新的函数length[i+1]
。
那不是和make-length
一样了?
是这样。
那我们将last-length
改名为make-length
也可以吗?
可以。只要保持函数参数和函数体内的名字一致即可。
那我们改名吧:
1 | ; length0 |
进一步,依照刚刚所说,我们只需要将length1
中最内层的函数应用添加一层
1 | ; length1 |
等等?为什么id
又出现了?
因为我们只能处理长度小于等于1的list。
如何解决此困境?
再次将make-length
传递给自身,这样每当我们需要更长的length[i]
的时候,就会自动将函数的处理能力+1。
就像这样:
1 | ; length1 |
这个函数的工作机理是,它展开的层数总是和喂给它的list
长度一样多。因为一旦层数不够长,他就会将make-length
应用于自身,增加多一次的处理。
试一试length1
能否工作:
1 | > (((lambda (make-length) |
很好,我们得到了一个能工作的length1
,事实上,这已经不再是length1
了,因为它能处理长度为2的list!甚至更长的!
这就和我们最初的length
一样了!
(如果你不明白这里的length1
为何能工作,可以试着再读一读这两个小节,或者手动推演一下这个lambda式子)
不止应用于length
对比一下我们的显式递归length
和newlength
:
1 | ; length |
这两个定义在形式上还是有差别,因为前者定义中的(length (rest lst))
,在后者定义中变成了((make-length make-length) (rest lst))
。
为了使它们表现的一致,我们将(make-length make-length)
抽象出来就好了,然后作为参数传进去,而且我们将参数取名为length
!
1 | ; length |
好了,看上去大功告成,可以把两个定义在形式上相同的地方抽象出来,这样就可以用在别的递归函数定义中了。
WAIT!还没完,我们试着用一用newlength
,试着对(apple)
求值:
1 | > (((lambda (make-length) |
我们需要将这个表达式的值求出来,然后应用到(apple)
上
1 | ((lambda (make-length) |
首先我们需要将(lambda (make-length) (make-length make-length))
应用于后面那个式子(记作A)上,得到(A A)
,即:
1 | ( |
然后继续求值,将B应用于C,也就是将B的参数make-length
用C进行代换,得到:
1 |
|
现在我们得到了形如(D E)
的式子,然后我们继续求值,你会注意到,E的形状和刚刚我们得到的(A A)
形状是一样的,所以我们会在这里一直循环下去。
这就很奇怪了,我们在把(make-length make-length)
抽象出来之前函数都是好的呀?
回想一下,make-length
的作用是生成一个新的length[i+1]
函数,然后将其应用于一个list。
但是我们把(make-length make-length)
抽象出来之后,我们无法得到这个length[i+1]
函数,因为Scheme是应用序求值策略,make-length
反复对自身进行应用。
因此,我们要想一个办法,先让make-length
的应用停一停,等到需要它对自身进行应用的时候,再继续。
这个方法并不复杂,就是将(make-length make-length)
包裹在一个lambda
表达式之中,即(lambda (x) ((make-length make-length) x))
。
1 | ; newlength |
然后我们再把它抽象出来。
1 | ; new newlength |
这样,我们就得能正确使用的newlength
了,就能继续我们的抽象工作了。
对比一下newlength
和原始length
,使用“;;;;;;”分隔开的部分,完全就是原始length
的定义,除了将原始length
定义中的define
替换成lambda
。而且这部分,和make-length
完全无关!
那么,我们将这部分抽象出来,使其变成一个函数应用。
1 | ( |
现在我们有了两部分,Y和F:
- Y部分是一个函数,它接受一个和原始
length
具有相同形式的函数,然后返回一个递归函数length
; - F部分,就是和原始
length
形式相同的匿名函数啦。
那么这个Y部分,就是我们的Y combinator!一般被称作应用序Y combinator。
化简一下:
1 | ; applicative-order Y combinator |
有了Y combinator,我们就能从匿名函数中定义递归函数了!
练习
你能写出一个简单的阶乘函数factorial
,然后用Y combinator将其变成匿名递归函数吗?