Lambda算子5b:How of Y
其实是 这篇文章的意译。有些东西省了。添了点私货。就有了下面的帖子。
虽然Y相当神奇。对它的推导也不完全是天外飞仙般无迹可寻。基本上我们为了解决让没有名字的函数能自我引用,一步一步抽象出了Y。所以知道Y的推导过程对我们程序员还是很有意义的:毕竟编程的过程也是抽象的过程。看看当年的老大们怎么从纷繁的表象里抽象出一般规律,对我们日后的思考应该大有好处。为了老大们能够试验,我就用JavaScript了。整个推导的过程非常像编程时的重构。我们提出一个粗略的解决方案,然后仔细观察,找出可以抽象的地方,进行抽象。得到一个更普适的结果后,继续重复重构的步骤,直到得到最优解。估计看这篇帖子的人都知道怎么玩儿小小JavaScript吧?再说有个浏览器就有了测试环境。废话少说,看代码。我们还是以阶乘函数为例。先看通常的写法:
1: function fact(n){
2: if(n == 0){
3: return 1;
4: }
5:
6: if(n > 0){
7: return n * fact(n - 1);
8: }
9: }
上面的JavaScript函数定义内部调用自己。这种调用可行的前提是我们用函数名指代函数定义。也就是说,
fact这个名字绑定的函数定义就是上面的函数体。如果我们不能通过名字来调用函数怎么办呢(就跟lambda算子一样)?也许有老大会问:为什么增加这个限制呢?不是自虐么?理由很简单:理论需要探求事物本质。记得奥卡姆剃刀吧?如无必要,毋增实体。函数名到底是必需元素,还是句法糖?这种研究方法也有实际的意义:再复杂的系统也是在简单但完备的基础上搭建起来的。强大的编程工具,总是基于于层层叠加的抽象,而最低级的抽象层总是非常简单。简单意味着透彻,简单意味着健壮。简单意味着灵活。简单意味着经济。问题是,到底简单到什么地步?怎么保证系统不至于简单到一无所用的地步?这和逻辑学家建立系统时总是要证明系统的正确性和完备性一个道理。而找到了Y,我们也就明白了,原来函数名绑定并非本质。
嗯,继续。函数
fact是递归的基本形式。既然我们不能直接在函数体内通过函数名调用另一个函数,我们至少可以把想调用的函数通过参数传进去。于是我们得到
fact2:
09: fact2 = function(himself, n){
10: return function(n){
11: if(n < 2){
12: return 1;
13: }
14:
15: return n * (something_expression_with_himself);
16: }
17: }
我用JavaScript里的匿名函数来强调lambda函数不具名的特征。这里用fact2纯粹为了方便。变量fact2本身并没有参与运算。如果我们这样调用:fact2(fact2, 3),那fact2这个函数不就可以调用自身了么?这也就是前面提到的自我引用的技巧:加一层抽象,从而把自己通过参数传给自己。现在我们要解决的就是,到底something_expression_with_himeself是什么东西。因为fact2和himself都必须递归调用自己,something_expression_with_himself从直觉上应该是himself(himself, n-1)开始。看到没有?函数体和调用方式不变,但n变成n-1了。和递归一致了哈:
19: fact3 = function(himself, n){
20: if( n < 2 ){
21: return 1;
22: }
23:
24: return n * himself(himself, n-1);
25: }
在你的JavaScript console里试验一下:fact3(fact3, 3) 的确返回6。什么,没有JavaScript console? 老大啊,上网的银能不用FireFox么?用了FireFox的程序员能不用FireBug么?快去下载吧。
下载完了?那我们继续。记得lambda算子里的函数只接受一个参数吧?可是fact3接受两个函数。所以我们要撒一点“咖喱”:
17: fact4 = function(himself){
18: return function(n){
19: if( n < 2 ){
20: return 1;
21: }
22:
23: return n * himself(himself)(n-1);
24: }
25: }
运行一下三。看见才能相信哈。fact4(fact4)(3) = 6, fact4(fact4)(4) = 24, fact4(fact4)(5)=120。。。。
现在的问题是,我们要把这个具体例子的递归方法抽象出来。现在我们需要把和自我引用无关的细节同自我引用本身分开。因为我们关心的是怎么运用自我引用来解决递归问题。我们可以先解决分离himself和n有关的代码。这样做是因为管理n的代码只与求阶乘有关,不是我们关心的重点。好比分离框架逻辑和业务逻辑。于是我们得到函数fact5。
37: fact5 = function(h){
38: return function(n){
39: var f = function(q){
40: return function(n){
41: if(n < 2){
42: return 1;
43: }
44:
45: return n * q(n-1);
46: }
47: }
48:
49: return f(h(h))(n);
50: }
51: }
运行几个例子增强点信心哈:fact5(fact5)(3) = 6, fact5(fact5)(4)=24。。。
注意函数f其实不用嵌在fact5里面。所以我们可以写成:
37: fucntion f(q){
38: return function(n){
39: if(n < 2){
40: return 1;
41: }
42:
43: return n * q(n-1);
44: }
45: }
46:
47: function fact5(h){
48: return function(n){
49: return f(h(h))(n);
50: }
51: }
现在就可以看出两件事了:一是新的函数
f不过是参数化的阶乘函数 ―― 递归部分变成参数(也是一个函数)
q了。第二,我们可以把f进一步抽象出来,剥离具体的阶乘部分,于是得到了Y:
69: function Y(f){
70: g = function(h){
71: return function(x){
72: return f(h(h))(x);
73: }
74: }
75:
76: return g(g);
77: }
现在我们可以用Y来实现阶乘了:
79: fact6 = Y(
80: function(h){
81: return function(n){
82: if(n < 2){
83: return 1;
84: }
85:
86: return n * h(n-1);
87: }
88: }
89: );
试一下,比如fact6(3) = 6, fact6(4) = 24。。。。
长出一口气。连写了4个小时,终于把关于Y的东西写完了。下面可以谈更宽泛的组合算子了。主要是S,K,和I三个算子。有个 变态编程语言unlambda就是靠解析SKI为生的。