轻松理解Y结合子——Javascript推导

本文详细解析了如何在JavaScript中利用Y组合子实现递归,通过多个步骤逐步推导,从原生递归到利用高阶函数和lambda演算,最终形成著名的Y组合子形式,为读者提供了一次深入理解递归实现和lambda演算的机会。

Y结合子(Y Combinator,也译作Y组合子),是在原生不支持递归的编程语言中利用lambda演算实现递归的一种方式,Y结合子在支撑递归的语言中没有什么实际的用途,更多是为了锻炼大家的程序逻辑思维,通过推演充分理解lambda和闭包。下面我们利用Javascript来一步步推导Y结合子。


原生递归

先从一个经典的递归算法——斐波那契数列讨论,以下是Javascript原生支撑的递归版本:
    var fibonacci = function (n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fibonacci(n - 1) + fibonacci(n - 2);     // 1
    }
    console.log(fibonacci(2));
或者
    var fibonacci = function (n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return arguments.callee(n - 1) + arguments.callee(n - 2);     // 2
    }
    console.log(fibonacci(2));

简单解释一下,注释1标注的代码行中,fibonacci是通过Javascript的闭包访问到的,是其父作用域中的fibonacci,也就是目标函数自己本身,从而实现了递归。注释2标注的代码行中,利用arguments.callee访问到函数本身。

第一步

假设,我们不想通过父作用域的(想纯粹使用lambda演算)或者arguments.callee调用自己,定义一个fibonacci的高阶函数——f,同时f接收一个function参数(其形式等同于f本身),其返回值就是fibonacci函数(目标函数)。换句话说就是f(f)展开以后就是fibonacci函数,而f的函数体中可以通过参数f,调用f(f)展开,得到函数本身,实现递归,代码如下:
    var f = function (f) {
        var fibonacci = function (n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            return f(f)(n - 1) + f(f)(n - 2);     // 3
        };
        return fibonacci;
    }
    var fibonacci = f(f);
    console.log(fibonacci(3));

第二步

注释3标注的代码行中,目标函数需要通过f(f)得到,形式不够简洁,可以利用lambda演算变换一下,首先构造一个函数g,把”f(f)“抽象出来。
    var f = function (f) {
        var g = function (n) {
            return f(f)(n);
        }
        var fibonacci = function (n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            return g(n - 1) + g(n - 2);     // 4
        };
        return fibonacci;
    }
    var fibonacci = f(f);
    console.log(fibonacci(4));

第三步

现在fibonacci 函数体的形式一样和期望的一样了,但是我们最后要把fibonacci 抽象成任意函数,现在代码行4中的函数g是通过闭包传递的,所以要把它变成参数传递。定义一个新的函数fun(fibonacci 的高阶函数),把fibonacci 包装起来,传递参数g函数,并返回fibonacci。
    var f = function (f) {
        var g = function (n) {
            return f(f)(n);
        }
        var fun = function (g) {     // 5
            var fibonacci = function (n) {
                if (n == 0) return 0;
                if (n == 1) return 1;
                return g(n - 1) + g(n - 2);
            };
            return fibonacci;
        }
        return fun(g);
    }
    var fibonacci = f(f);
    console.log(fibonacci(5));

第四步

代码行5中,fun应该从外部传进来,所以最后把fun抽象出来,同样上一步一样的技巧,把fun也变成参数传递
    var Y = function (fun) {
        var f = function (f) {     // 6
            var g = function (n) {     // 7
                return f(f)(n);
            }
            return fun(g);
        }
        return f(f);     // 8
    }

    var fun = function (g) {
        var fibonacci = function (n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            return g(n - 1) + g(n - 2);
        };
        return fibonacci;
    }
    var fibonacci = Y(fun);
    console.log(fibonacci(6));

第五步

现在需要把6、7行f,g的定义处内联到引用处,就能得到Y结合子最后的形式,不过在内联之前注意注释的第8行,f引用了两次,内联的时候就会出现重复代码,再次运用lambda演算技巧,”包装传参“。
把f(f)抽象成函数recur。
    var Y = function (fun) {
        var f = function (f) {
            var g = function (n) {
                return f(f)(n);     // 9
            }
            return fun(g);
        }
        var recur = function (f){
            return f(f);
        }
        return recur(f);
    }

    var fun = function (g) {
        var fibonacci = function (n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            return g(n - 1) + g(n - 2);
        };
        return fibonacci;
    }
    var fibonacci = Y(fun);
    console.log(fibonacci(7));

第六步

现在再看g、f、recur的引用处,发现各只有一个(注意:不包括参数传递的g、f),现在内联就不会有重复代码了。在内联之前把注射第9行代码修改一下,让Y结合子可以接受任意的形参个数的函数,如下:
return f(f).apply(null, arguments);
最后内联g、f、recur三个函数,得到著名的Y结合子:
    var Y = function (fun) {
        return (function (f) {
            return f(f);
        })(function (f) {
            return fun(function () {
                return f(f).apply(null, arguments);
            });
        });
    }

    var fibonacci = Y(function (g) {
        var fibonacci = function (n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            return g(n - 1) + g(n - 2);
        };
        return fibonacci;
    });
    console.log(fibonacci(8));
注意最后的到的Y结合子中全部都是lambda函数,是一个完全的lambda演算,直接看Y结合子的最终形式比较吃力,读者能看懂第六步没有内联的形式,大致上就能充分理解Y结合子。^_^

扩展阅读:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值