A brief introduction to Y-Combinator
为什么要写这篇:当我们谈论计算机科学时,lambda 演算算得上其中最抽象,同时又是最优雅的一部分了。它可以构建出一些很神奇的东西,例如神奇的丘奇数(church numeral),用函数定义了所有的自然数和运算。3 月初出于兴趣了解了一下什么是 Y-组合子,当时恰好手写了一些笔记,但更多是对于别人的知识的整理,我个人水平有限,对于这块理论学习不多,就当作写博客练练手吧,如果能让读者产生一丝“计算机科学还挺有意思”的想法就再好不过啦~
当我们需要创建一个递归函数时,如果写成 lambda 表达式,我们可以写成
f = lambda x: 1 if x == 0 else x * f(x - 1)但是假如我给定一个奇怪的规则:不允许使用赋值符号,也就是不能用 f = ...,此时我们创建的就是匿名函数,还能实现同样的递归函数吗?
在上面的例子中,调用 f(4) 可以得到 24,我们要做的就是定义一个 ?,然后立即调用它(因为不能把它存下来),即 (?)(4) 的结果是 24。这看上去挺难的,我们要在 ? 的函数体内递归调用自己,而此时 ? 甚至都没有名字!
为了做到这件事,我们要转换下思维,用上“高阶函数”的概念:不再局限于函数参数只能是一个值,它还可以是一个“函数”。假设有这样一个神奇的函数 g,把 f 作为参数传给它,它返回我们需要的 ?。这时,我们可以在 g 的函数体内调用 f 了
g = lambda f: (lambda x: (1 if x == 0 else x * (f f)(x - 1)))这里为什么是 (f f) 呢?因为我们想要实现 g(x-1) 的效果,但我们手头能用的只有 f 这个传进来的参数,不妨想象 f 跟 g 有类似的性质:f 传入一个函数也能返回想要的 ?。那么自然就需要先让 f 作用于自身,得到 ?,再进行 ?(x-1) 的调用。这样解释似乎有些从结果反过来说的意味,事实上如果接受了这种写法,我们要实现的 ? 也就得到了:g(g)
g(g)(4)
= (lambda f: (lambda x: (1 if x == 0 else x * (f f)(x - 1))))(g g)(4)
= (lambda x: (1 if x == 0 else x * (g g)(x - 1)))(4)
= 4 * (g g)(3)
= 4 * (lambda x: (1 if x == 0 else x * (g g)(x - 1)))(3)
= 4 * (3 * (g g)(2))
= 4 * (3 * (lambda x: (1 if x == 0 else x * (g g)(x - 1)))(2))
= 4 * (3 * (2 * (g g)(1)))
= 4 * (3 * (2 * (lambda x: (1 if x == 0 else x * (g g)(x - 1)))(1)))
= 4 * (3 * (2 * (1 * (g g)(0))))
= 4 * (3 * (2 * (1 * (lambda x: (1 if x == 0 else x * (g g)(x - 1)))(0))))
= 4 * (3 * (2 * (1 * 1)))
= 4 * (3 * (2 * 1))
= 4 * (3 * 2)
= 4 * 6
= 24上面这段推导来自于 GPT,虽然此时我们还用了 g 这个名字,但 g 的表达式可是一个纯纯的函数表达式,把那串很长的 lambda 表达式替换 g,我们实际上得到了前面提到的匿名递归函数。想想还是挺妙的。
这种思想到底是怎么来的呢?继续考虑 g,如果对于一个递归函数 f,调用 g(f) 还是得到 f,这里的 g(f) = f 看上去跟不动点方程的表达式很像,我们把 f 称为 g 的不动点。
怎么求解不动点呢?假设存在一种解法可以求得 g 的不动点, 设为 Y,有
f = Y g对于不动点方程不断迭代下去,
f = g(g(g(...)))这个无限长序列假设可以开方的话,可以简写成
f = G(G)
= g(G(G))把 G 写成定义的形式,就是
G = λG. g(G(G))由 alpha-变换(函数的参数名可以任意替换,都是等价的),换名得到
G = λx. g(x x)代入 Y g = f,得
Y g = f
= G(G)
= (λx. g(x x))(λx. g(x x))那么 Y = λg. (λx. g(x x))(λx. g(x x)),换名得到
Y = λf. (λx. f(x x))(λx. f(x x))可以验证 Y 是满足不动点方程,即 f(Y) = Y 的,这便是大名鼎鼎的 Y-组合子,它是一种通用的构造递归函数的算子。
用 JavaScript 的箭头函数来实现,可以是
const Y = f => {
const g = x => f(y => x(x)(y));
return g(g);
};
Y(fac => n => {
return n <= 1 ? 1 : n * fac(n - 1);
})(3); // 6或者是
const Y = t => (f => f(f))(
f => t(
x => (f(f))(x)
)
);
Y(fac => n => {
return n <= 1 ? 1 : n * fac(n - 1);
})(3); // 6回到一开始提出的问题,我们不可以写
const f = fac => n => {
return n <= 1 ? 1 : n * fac(n - 1);
}但是有了神奇的魔法 Y 之后,把上面的错误写法用魔法变换一下,就变成了可以正确计算的黑盒子!
const factorial = Y(fac => n => {
return n <= 1 ? 1 : n * fac(n - 1);
});
console.log(factorial(3)); // 6lambda 演算系统是一个形式系统,只有三条语法规则
x | λx. expr | expr expr分别表示变量定义、函数定义、函数调用和两个操作
alpha-变换:变量任意替换
beta-规约:两个 λ 项,把右侧替换左侧,剩下一个 λ 项可以证明,lambda 演算和图灵机的计算能力是等价的,而后者是使得今天一切通用计算机得以成立的计算模型。
参考知乎文章:
