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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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结合子。^_^

扩展阅读:

### 如何在不同编程语言中从数组或列表中移除 NaN 值 #### JavaScript 中移除 NaN 值 在 JavaScript 中可以利用 `filter()` 方法来实现这一目标。`filter()` 方法会创建一个新的数组,其中只包含通过测试的元素。对于检测是否为 NaN 的情况,可以使用全局函数 `isNaN()` 或者更推荐的方式是使用 ES6 提供的 `Number.isNaN()` 来判断。 以下是具体的代码示例: ```javascript let myArray = [1, 2, NaN, 4, NaN, 6]; let filteredArray = myArray.filter(item => !Number.isNaN(item)); console.log(filteredArray); // 输出: [1, 2, 4, 6] ``` 此代码片段展示了如何过滤掉数组中的所有 NaN 值[^1]。 #### Python 中移除 NaN 值 在 Python 中处理含有 NaN 值的数据通常借助于 NumPy 库或者 pandas 库。NumPy 是一种用于科学计算的基础库,提供了强大的多维数组对象以及各种派生的对象(如掩码数组和矩阵)。pandas 则是一个基于 NumPy 构建的强大数据分析工具包。 如果仅依赖标准库而不引入额外模块的话,可以通过简单的列表推导式完成任务: ```python import math my_list = [1, 2, float('nan'), 4, float('nan'), 6] filtered_list = [item for item in my_list if not math.isnan(item)] print(filtered_list) # 输出: [1, 2, 4, 6] ``` 当数据量较大时建议采用 NumPy 方式操作更为高效简洁: ```python import numpy as np arr = np.array([1, 2, np.nan, 4, np.nan, 6]) cleaned_arr = arr[~np.isnan(arr)] print(cleaned_arr) # 输出: [1. 2. 4. 6.] ``` 上述两种方法均能有效清除列表/数组内的 NaN 元素[^3]。 #### PHP 中移除 NaN 值 PHP 并不像其他高级脚本语言那样内置支持浮点数特殊值 (infinity 和 nan),因此需要手动验证数值的有效性再决定是否保留下来。下面展示了一种可能的做法——运用自定义辅助函数配合 array_filter 实现功能需求: ```php function is_not_nan($value){ return $value === $value; // 运算符 '===' 可区分正常数字与 NaN } $numbers = [1, 2, log(-1), 4, sqrt(-9), 6]; // 注意:log(-1) 和 sqrt(-9) 将生成 NAN 类型的结果 $resultant_array = array_filter($numbers , "is_not_nan"); print_r(array_values($resultant_array)); /* 输出: Array ( [0] => 1 [1] => 2 [2] => 4 [3] => 6 ) */ ``` 这段程序说明了即使面对复杂场景也能灵活调整策略达成目的[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值