重新发明 Y 组合子:不用 λ演算那一套
重新发明 Y 组合子:不用 λ演算那一套
目录
- 1. 一类奇妙的函数
- 1.1. 三个例子
- 1.2. 构造方法
- 1.3. 程序分解
- 2. X 组合子
- 3. Y 组合子
- 4. 结语
1 一类奇妙的函数
1.1 三个例子
函数
(lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
以一个函数为参数,当这个参数是函数本身时,返回计算阶乘的函数
函数
(lambda ($) (lambda (l) (if (null? l) 0 (+ 1 (($ $) (cdr l))))))
以一个函数为参数,当这个参数是函数本身时,返回计算列表长度的函数
函数
(lambda ($) (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (($ $) (- n 1)) (($ $) (- n 2)))))))
以一个函数为参数,当这个参数是函数本身时,返回计算 Fibonacci 数的函数
1.2 构造方法
上面三个函数的构造方法很机械。
以第一个为例。
常见的计算阶乘的函数
(define fact (lambda (n) (if (< n 2) 1 (* n (fact (- n 1))))))
该函数的名字是
fact
该函数的函数体是
(lambda (n) (if (< n 2) 1 (* n (fact (- n 1)))))
该函数的名字在该函数的函数体中的上下文是
(lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
以一个函数为参数,当参数是自身时返回计算阶乘函数的函数就是
(lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
1.3 程序分解
重复上一节的最后两个步骤。 我们是从【fact 函数的名字在 fact 函数的函数体中的上下文】
A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
构造【以一个函数为参数,当参数是自身时返回计算阶乘函数的函数】
B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
的。但是正如你看到的那是无理由的机械构造。
那么问题来了。B 真的能由 A 构造吗?
对比一下
;; 只有一点点不同 A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) ;; 一个是 ~ B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1)))))) ;; 一个是 ($ $)
很像数学
a * c (a + b) * c = a * c + b * c
式子
(a + b) * c
确实可以由式子
a * c
来构造。
这是乘法的【结合律】。
程序也有【结合律】吗?
数学中乘法的【结合律】是用【因式分解】证明的。
程序里面怎么做【因式分解】?
用【上下文】。
分解谁?
分解
B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
因为,我们想看看它是不是由
A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
构造的。
怎么分解?
取
($ $)
的上下文,因为它是两者唯一的不同点。
怎么取它的上下文呀?包含它的表达式起码有
1. ($ $) 2. (($ $) (- n 1)) 3. (if (< n 2) 1 (* n (($ $) (- n 1)))) 4. (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))) 5. (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
5 个。取它在哪个表达式中的上下文呀?
都取试试看
1. (lambda (~) ~) 2. (lambda (~) (~ (- n 1))) 3. (lambda (~) (if (< n 2) 1 (* n (~ (- n 1))))) 4. (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) 5. (lambda (~) (lambda ($) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))))
1 是废话,不要。
2 和 3 少了n 的信息,也不要。
5 多了无用的 dollar ,也不要。
4 提供的绑定不多不少,就要它了。
而且回过头来看 4 正好就是 A
4. (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
现在更加确定 A 能构造出 B ,即 B 能分解出 A 来。
把 4 装回 B 中去。【装逼去!】
A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1)))))) 4. (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) ;; 装回去 B = (lambda ($) ((lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) ($ $)))
这样装 B 对不对呀?
不对。
在真 B 中 (dollar dollar) 在求值进入到里面那个函数之后,如果 n 小于 2 就不用求值,如果 n 不小于 2 才求值。
而装 B 中 (dollar dollar) 不用等到求值进入到里面的那个函数,求值才刚进入外面这个函数就必须求值了。
所以说装 B 不对。
怎样把装 B 修复成真 B ?
装 B 把真 B 中的一个求值提前了,那我们就把它延迟一下就行了。
B = (lambda ($) ((lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))) (lambda (n) (($ $) n))))
现在 B 中分解出 A 来了,证实了能从 A 构造出 B 来,我们的程序分解结束了吗?
没有,A 插入 B 中太深,拔出一点点来才好。
B = ((lambda (^) (lambda ($) (^ (lambda (n) (($ $) n))))) (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))) A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
这种深度最舒服了。
2 X 组合子
把剩下的两个例子也分解一下和上一节的放在一起
((lambda (^) (lambda ($) (^ (lambda (n) (($ $) n))))) (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))) ((lambda (^) (lambda ($) (^ (lambda (list) (($ $) list))))) (lambda (~) (lambda (list) (if (null? list) 0 (+ 1 (~ (cdr list))))))) ((lambda (^) (lambda ($) (^ (lambda (n) (($ $) n))))) (lambda (~) (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (+ (~ (- n 1)) (~ (- n 2)))))))
三个函数的第一行都是一样的,抽象出来吧
(lambda (^) (lambda ($) (^ (lambda a (apply ($ $) a)))))
取个什么名字好?
X 呀,因为之前又是 B 又是 插入太深的。
(define X (lambda (^) (lambda ($) (^ (lambda a (apply ($ $) a))))))
现在有了 X 了,用一句话说清楚 X 有什么用?
X 能将【有名字的递归函数的名字在函数体中的上下文】转换成【以一个函数为参数,当参数是它本身时,返回递归函数的函数】。
X!这么复杂的表述方式,但是老子还是听懂了。那有没有
能将【有名字的递归函数的名字在函数体中的上下文】转换成【递归函数】
的函数。
有,就是下一个字母 Y 。
3 Y 组合子
(define Y (lambda (^) ((X ^) (X ^))))
4 结语
老子总算懂 Y 组合子了。但这个过程中经历一次装B失败。
其实如果仔细研究最简单的
(define fact (lambda (n) (if (< n 2) 1 (* n (fact (- n 1))))))
这种类型的递归函数,就可能避免这次装B失败。
【递归函数总要使用 if 或者 cond 】
因为它们特殊的求值规则,能够延迟或者取消对递归部分的求值,避免无穷递归下去。