在上一篇「离题」的文章中,我们介绍了如何单纯通过几个简单概念实现一个自然数的概念。这也告诉我们,其实函数式编程一个最核心的内容就是用最少的概念派生性地产生更多的概念来实现功能。这个不像Java之类的对象式语言需要的原生概念非常多,然后又需要更多的派生概念解决问题。

但是,其实我们在上面的文章中一直避而不谈,就是其实我们使用了「递归」这个概念,这个概念是必须要的「原生」概念吗?还是一个可以用「原生」概念替代的「派生」概念。这一章,我们就试图解决这个问题。

Lambda演算

首先,我们要回到\(\lambda\)演算的定义,就是作为一个函数式编程,至少要实现一个\(\lambda\)演算(当然还有更为复杂组合子的模型,这里暂时不考虑):

  1. 变量:\(x\)
  2. 抽象:\(\lambda x. M\)
  3. 应用:\((M N)\)

相当于函数式编程,最基本的就是实现这三个概念。而在Python中这三个的体现就是变量匿名函数匿名函数带入值。例如下面的例子中x就是变量,f =g =后面的部分就是匿名函数/抽象,f(1)g(lambda x: x + 1, 1)就是应用:

f = lambda x: x + 1
f(1)

g = lambda h, x: h(x)
g(lambda x: x + 1, 1)

但是事实上,\(\lambda\)事实上是不够的,因为\(\lambda\)演算只提供了一套符号代换逻辑,譬如上面的fg的例子,事实上,我们通过\(\lambda\)演算,只能求出:

>>> f(1)
1 + 1

>>> g(lambda x: x + 1, 1)
(lambda x: x + 1)(1)
(1 + 1)

所以,第一个要增加必不可少的是化简/计算的逻辑,就是涉及到四则运算、布尔运算的逻辑,当然这些也可以用符号/函数的概念派生出来,像我们上一章做的一样。

我们再来看对于一个递归函数还要什么,这里我们实现一个2的指数的函数:

power_of_2 = lambda x: 1 if (x == 0) else 2 * power_of_2(x - 1)

我们首先发现,对于一个可以在现实生活中发生作用的函数而言,「分支」是必要的,它会终止不必要的无穷循环。

其实,我们发现了一个问题,就是power_of_2再没有完全实现前,就已经调用了power_of_2,这要求我们的编译器需要有个特别的处理,可以忽略这件事。并且我们发现,上面的式子无法用\(\lambda\)演算来表达。这就出现了问题。「递归」的概念是必须原生地作为程序语言的一部分,还是我们可以用过\(\lambda\)演算来实现。这就回到了我们这一篇要讨论的Y组合子的概念中来了。

Y组合子

我们重新看看这个问题,事实上,要解决它很容易,就是把power_of_2这个也当做变量通过lambda传入,即应该是如下形式:

# 注意这里是伪代码不可执行
power_of_2 = lambda f: ???? lambda x: 1 if (x == 0) else 2 * f(x - 1)

其实我们要做的一件事就是如何实现在应用中可以循环代换,可以无限地迭代自己。类似这个样子:

\[f(g) = g(f(g)) = g(g(f(g))) = ...\]

或者用\(\lambda\)演算的符号表达就是

\[(\lambda x. N) g = g((\lambda x. N) g)\]

我们只要找到符合这种代换的符号集\(N\)即可。大名鼎鼎的Haskell Curry就找到了一个这样的答案,即\(Y = \lambda f.(\lambda x. f(x x))(\lambda x. f(x x))\)。一般我们把这个YY组合子。我们可以验算一下上面的式子。

\[\begin{align} Y g && = && \lambda f.(\lambda x. f(x x))(\lambda x. f(x x)) g\\ && = && (\lambda x. g(x x))(\lambda x. g(x x)) \\ && = && g((\lambda x.g(x x))(\lambda x. g(x x))) \\ && = && g(Y g) \end{align}\]

注:需要在这里解释一下其中的推导过程。第二行实际上将\(g\)引用到了\(Y\)里;第三行是将\((\lambda x. g(x x))\)应用到了\((\lambda x. g(x x))\)里。

我们可以按照上面的式子自己实现一个Python版本的Y

Y = lambda f: (lambda g: f(g(g)))(lambda g: f(lambda y: g(g)(y)))

我们实现power_of_2现在仅需要类似def一样的声明即可:

power_of_2 = Y(lambda f: lambda x: 1 if (x == 0) else 2 * f(x - 1))

当然,如果遇到多元函数,我们可能需要一个Y组合子的推广——Z组合子:

Z = lambda f: (lambda g: f(g(g)))(lambda g: f(lambda *y: g(g)(*y)))

比如下面这个求指数的函数power

power = Z(lambda f: lambda x, n: 1 if (n == 0) else x * f(n - 1))
power(2, 3)

总结一下,到此为止,我们通过Y组合子的方式,仅通过\(\lambda\)演算的概念,实现了「递归」的概念。因此,可以论证出「递归」这个概念可以在函数式编程语言中是「派生」的,而不需要「原生」支持。我们仅仅需要\(\lambda\)演算就能模拟自然数、运算、递归这些概念了。当然,这篇文章仅仅是作为一个引子,让你看到函数式编程的表达能力以及理论基础,还有和数学特别是抽象代数、符号逻辑的联系。后面我们将慢慢见证一个个数学概念如何转变为可以应用的例子。