在 Lambda 演算中,对于一个函数 f,如果有一个值 x 满足 f x = x,那么称 x 是 f 的一个不动点。我们有能力利用递归去构造出「任意」函数的一个不动点,并且可以形式化地写出将一个函数映射成其不动点的函数,其中有两个例子:Y 组合子和图灵组合子。
Y 组合子的定义是:
1 | Y : (A -> F) -> A |
图灵组合子的定义是
1 | T : (A -> F) -> A |
这引出了一个问题:对于任意函数,如果这个函数能将任意函数映射成其不动点,那么都能求出相同的不动点?
尝试解决
如果要形式化这个问题,类型签名会巨长无比。设 O 和 P 是两个能求不动点的函数,尝试找反例。
令 f = P, 那么 P P 表示满足 P x = x 的 x,也就是 x 的不动点是其自身,可以认为 x 首先得是一个能接受无穷多参数的函数,而且 x 的类型签名包含无穷嵌套,但不太清楚如何处理。
尝试结合高中的直觉认识这个问题:假如有一个抛物线 f(x) = x^2 - 8, 显然对于这个函数是有两个不动点的。如果设置一个从左往右和一个从右往左找不动点的函数,就可以找到不同的结果。然而在 Lambda 演算里面,不动「点」也可以是「函数」,使得「左」和「右」的概念不再存在,并且其所在空间未必能被这样遍历。即使能遍历,通过遍历找到不动点也是难以形式化的,只能作为直觉而不能作为数学证明。
但我们可以尝试看看 Y 组合子和图灵组合子是否一样。
尝试给图灵组合子引入参数:
1 | T = \y => y ((\x => \y => y (x x y)) (\x => \y => y (x x y)) y) |
但实际上只是很失败地证明了 T 是求不动点的函数,而可以认为 T 的定义就是 T = \y => y (T y). 你可以想象 T 是一个迭代过程,上一步求出 T y,然后令 T’ y = y (T y). 但实际上,T y 已经是最终结果了,迭代过程根本不存在,换言之根本没有空间去指定所谓「方向」、「初值」。
对于 Y 组合子,
1 | Y = \f => f ((\x => f (x x)) (\x => f (x x))) |
也只是很失败地证明了 Y = \f => f (Y f), 式子一模一样。实际上求值策略如果采用传值的话,Y 和 T 都会导致无穷递归,因为它们的特点都是先流氓地定义上一部迭代已经是「最终结果」了。
但为什么在尝试构造匿名递归函数的情景,Y 组合子这类东西真的能起到作用呢?
以求 1 + 2 + … + n 的函数 f 为例,我们可以构造出这样的函数的一个生成器 g:
1 | g = \f n => ifThenElse (isZero n) 0 (add n (f (pred n))) |
g 的不动点就是所要求的 f,因此
1 | f n = (\x => g (x x)) (\x => g (x x)) n |
注意这里如果 isZero n,那么 g … n 可以被直接变成 0,因此真的能求出来值。如果只是求函数,就像求函数不动点一样,实际计算的时候永远求不出来,只是在概念上存在。再例如如果直接定义 f = f,这个 f 在概念上也存在,但显然没有具体的值。谈论不动点的「值」是没有意义的,同时也不存在「唯一性」。
在函数求值的情况下,其本质上和直接定义递归函数是一样的,因此只要递归函数相同,不动点函数的具体实现最终都可以化简成普通的递归操作,也就是 Y = \f => f (Y f),并不会影响其求出来的值(如果能求出来的话)。
总结
在 Lambda 演算中遇到和「不动点」有关的问题,不能把不动点看成具体的值,而是一个「概念」。概念之间可以「相同」,在应用一个参数也可以变成具体的值,但不一定是唯一的。