函数式编程的数学基础(四)邱奇数的除法,递归

1,118 阅读2分钟

邱奇数的除法实现较为困难。

但有了邱奇数的减法和比较,那么我们大致可以想到,如果能够不断把被除数减掉除数,直到它变得小于除数,我们就可以实现邱奇数除法了。

然而问题在于,我们如何在Lambda演算中实现这种“循环”?

最容易想到的是用递归代替循环,那么,Lambda函数能否直接递归呢?

答案是不行,我们使用一个Lambda函数前必须要先定义它。

Y Combinator

于是我们尝试构造一个可以递归的环境,假设有一个生成器函数YY,它能够提供给一个函数f以包含自身的上下文,即:

f=Y λf.(......) f = Y\ \lambda f.(......)

这样我们就可以在省略号处正常编写递归函数了。于是问题的关键转化为为,如何编写实现这个函数YY,我们首先写一个Y的形式,此处g应该是我们前面所写用于生成f的函数。

Y=λg.? Y = \lambda g. ?

我们希望给g传入f,即:

Y=λg.(g f)Y = \lambda g.(g\ f)

但我们又没有f,为了生成f,我们又要调用g,于是要写成:

Y=λg.(g (g (g ......)))Y = \lambda g.(g\ (g\ (g\ ......)))

看起来这样无穷调用,写不出来。

我们来换个思路,我们没有必要构造一个真实的f,我们只需要给传入一个跟f完全一致的函数就可以了。我们可以写一个虚假的F

F=λx.(g f x)F = \lambda x.(g\ f\ x)

但这里面仍然用到f,因为F完全等价于f,所以我们可以写:

F=λx.(g F x)F = \lambda x.(g\ F\ x)

这个写法仍然递归,但是因为F是我们自己定义的,所以我们可以构造一个把函数传给自己的结构

λF.(F F)\lambda F.(F\ F)

再把F改写做:

F=λF.(g (F F))F = \lambda F.(g\ (F\ F))

于是最终写法是:

Y=λg.(λF.(F F)) (λF.λx.(g (F F) x))Y = \lambda g.(\lambda F.(F\ F))\ (\lambda F.\lambda x.(g\ (F\ F)\ x))

注:如果不考虑实际编程产生死循环,Y组合子可以写作更简单的形式:

Y=λg.(λF.(F F)) (λF.(g (F F)))Y = \lambda g.(\lambda F.(F\ F))\ (\lambda F.(g\ (F\ F)))

这样我们就有了函数递归的能力,我们不妨把公式翻译成JavaScript代码验证下我们的推导:

const Y = g => (F => F(F))(F => x => g(F(F))(x));
const sum = Y(sum => n => n > 0 ? n + sum(n - 1) : 0);
sum(100); //5050

由此我们可以看到,无论语言是否支持递归,只要函数具有闭包性质和一等公民身份,我们都可以基于lambda理论实现递归。

这里的特殊函数Y,我们把它称作Y Combinator,即Y组合子

邱奇数除法

有了递归,我们实现邱奇数除法就变得顺理成章了。

div=Y λdiv.λm.λn.((less m n) 0 (add(div (minus m n) n) 1))div = Y\ \lambda div.\lambda m.\lambda n.((less\ m\ n)\ 0\ (add (div\ (minus\ m\ n)\ n)\ 1))

我们还可以顺道定义取余运算

mod=Y λmod.λm.λn.((less m n) (minus m n) (mod (minus m n) n))mod = Y\ \lambda mod.\lambda m.\lambda n.((less\ m\ n)\ (minus\ m\ n)\ (mod\ (minus\ m\ n)\ n))

总结

至此,我们已经实现了邱奇数的加减乘除,在这个过程中,我们还顺道实现了分支和循环(递归)两种逻辑,这样,我们推导出一组跟编程语言环境相近的lambda演算基础设施。

在整个学习过程中,你应该对一些函数式编程中常见的概念有一定体会:

  1. 函数是一等公民:在lambda演算中,函数是函数、函数是数据、函数是参数、函数是返回值、函数也是表达式,应该说,在原版lambda演算中,并不存在所谓“二等公民”,一切公民都是函数。
  2. 高阶函数:在lambda演算中,并不存在“非高阶函数”,一切函数都是以函数为参数、以函数为返回值的。
  3. 柯里化:在lambda演算中,并不支持多参数的函数,所以只能通过柯里化的形式表达多参数函数。
  4. 纯函数/不可变性:在lambda演算中,并不存在变量和赋值,所以更不可能改变变量的值。

实际上,因为lambda演算属于纯数学,它的设计并不在乎性能和人类编写方便,所以多数编程语言不会直接使用lambda的邱奇数、Y组合子等作为基础设施。

然而,正因为lambda演算达成了图灵完备,这让我们可以从其中获得制造计算机和设计编程语言的灵感。后世也在这个思路的基础上发展了组合子逻辑、类型论、基于范畴论的计算理论,这些都带来了编程领域的新发展。