Y组合子
Table of Contents
在这篇文章里,我会尝试推导出Y组合子,这是函数递归理论最重要的成果之一
Y组合子
在某些情况下可以不用给函数取名字,下面代码就是用匿名函数实现了“6加1”:
((lambda (x) (+ x 1)) 6) ;; => 7
可不可以用匿名函数来表达递归过程呢?比如:
(define fact (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))
在最后一行的使用了 fact 这个函数名字来产生递归调用,看起来好像无法用匿名函数来实现递归。实际上这个观点是错误的,后面一步一步地改造fact函数,来推导出整个过程
把fact作为参数传递
(define op-maker (lambda (op) (lambda (x) (op x))))
使用类似上面的模板代码把最后一行的 fact 作为参数传递进去:
(define fact-maker (lambda (procedure) (lambda (n) (if (zero? n) 1 (* (procedure (- n 1)) n))))) ;; fact-maker ; => #<procedure:fact-maker> ;; ((fact-maker fact-maker) 5) ; *: contract violation ; expected: number? ; given: #<procedure> ; argument position: 1st
上面代码不工作,因为 fact-maker 返回值是一个 匿名函数 ,而 (* (fact-maker (- n 1)) n) 要求 (fact-maker (- n 1)) 是一个数字
用下面的代码来修复这个问题:
(define fact-maker (lambda (procedure) (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1))))))) ((fact-maker fact-maker) 5) ; => 120
至今为止,我们成功地把函数名从过程体里面去掉了,但是我们在调用的时候还是要把函数名 fact-maker 传入
去除函数名
((fact-maker fact-maker) 5) 等价于 (fact 5), 因此可以重新定义fact函数为:
(define fact ((lambda (procedure) (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1)))))) ; fact-maker (lambda (procedure) (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1)))))) ; fact-maker )) (fact 5) ; => 120
实际上可以完全不定义fact的名字,直接调用:
( ((lambda (procedure) (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1)))))) ; fact-maker (lambda (procedure) (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1)))))) ; fact-maker ) 5) ; => 120
这一大堆东西已经可以实现不定义函数名来完成递归调用了。接下来要把和fact函数相关的逻辑抽象出来,推广到通用过程中
分离业务逻辑
首先,把计算阶乘的逻辑分离出来作为一个参数,这样一来其他的计算逻辑,可以作为通过这个参数传递进去
(define F (lambda (n) (if (zero? n) 1 (* n ((procedure procedure) (- n 1))))))
这并不难完全满足需求,还需要继续变换。我们知道 (h arg) 等价于 ( (lambda (x) (h x)) arg),由此可以把 ((procedure procedure) (- n 1)) 替换成:
((lambda (arg) ((procedure procedure) arg)) (- n 1))
把这个替换带 F 函数中:
(define F (lambda (n) (if (zero? n) 1 (* n ((lambda (arg) ((procedure procedure) arg)) (- n 1))))))
接着把 ((lambda (arg) ((procedure procedure) arg)) (- n 1)) 抽象成一个参数:
(define F ((lambda (func-arg) (lambda (n) (if (zero? n) 1 (* n (func-arg (- n 1)))))) (lambda (arg) ((procedure procedure) arg))))
开始F的定义是:
(define F (lambda (n) ... < procedure >))
现在F的定义是:
(define F ((lambda (func-arg) (lambda (n) ...)) < procedure >))
而<procedure>的定义是:
(lambda (arg) ((procedure procedure ) ...) ...)
代入步骤2的结果
现在把刚才的结果代入到步骤2的结果中:
(define fact ((lambda (procedure) ((lambda (func-arg) (lambda (n) (if (zero? n) 1 (* n (func-arg (- n 1)))))) (lambda (arg) ((procedure procedure) arg)))) (lambda (procedure) ((lambda (func-arg) (lambda (n) (if (zero? n) 1 (* n (func-arg (- n 1)))))) (lambda (arg) ((procedure procedure) arg)))))) (fact 5) ; => 120
注意 ((lambda (func-arg)… 之前的两个括号:
...... ((lambda (func-arg) < body-using-func-arg >) (lambda (arg) ...))
计算结果和下面是一样的:
((lambda (arg) ((procedure procedure) arg)) (- n 1))
两者的区别是: 第一种形式参数是一个函数,而第二种形式的参数一个数字
替换fact逻辑
仔细研究下面的代码:
(lambda (func-arg) (lambda (n) (if (zero? n) 1 (* n (func-arg (- n 1))))))
(lambda (func-arg) …) 这段代码包含了计算阶乘的所有逻辑,如果把它定义成 _F*_:
(define F* (lambda (func-arg) (lambda (n) (if (zero? n) 1 (* n (func-arg (- n 1))))))) (define fact ((lambda (procedure) (F* (lambda (arg) ((procedure procedure) arg)))) (lambda (procedure) (F* (lambda (arg) ((procedure procedure) arg))))))
我们可以用 任何的其他业务逻辑 作为 F* 来替换 计算阶乘的特定逻辑 。唯一留下的问题在于:还需要在外面定义F*
把F*作为参数传入
把 fact 重新命名为 Y , F* 作为 Y 的参数 F 传入:
(define Y (lambda (F) ((lambda (procedure) (F (lambda (arg) ((procedure procedure) arg)))) (lambda (procedure) (F (lambda (arg) ((procedure procedure) arg))))))) ((Y F*) 5) ; => 120 ((F* (Y F*)) 5) ; => 120
抽取重复的lambda定义
实际上(lambda (procedure) (X (lambda (arg) ((procedure procedure) arg))))被重复创建了两次,可以用 let 来简化:
(define Y (lambda (F) (let ((W (lambda (procedure) (F (lambda (arg) ((procedure procedure) arg)))))) (W W)))) ((Y F*) 5) ; =>120 ((F* (Y F*)) 5) ; =>120
这就是Y组合子的定义。如果利用 define 的语法糖,可以写得更简洁:
(define (Y F) (define (W P) (F (lambda (x) ((P P) x)))) (W W))
意义
Y组合子也被称为 不动点的高阶函数 , 可以很容易地证明:
Y(F) = F ; F是一个函数
它的意义在于:
- 实现匿名函数的递归
- 递归可以不依赖栈