函数式编程的数学基础(一)lambda演算,邱奇数加法与乘法
lambda演算是函数式编程的数学理论基础。但是我们在互联网上找到的函数式编程资料总是充满了"柯里化"、"纯函数"、"一等公民"等等听不懂的名词,很少有认真讲lambda演算的资料。
那么,lambda演算究竟是什么?它究竟解决了什么问题?它的价值究竟是什么?
在本系列中,就让我们通过实际学习来回答上面的问题,感受lambda演算的数学之美。
替换:人类的本能思维
任何数学概念都抽象自人类的朴素思维。我们看一个例子:
请把🍊🍌🍐🍊🍊中的🍊替换成🍐
在这句话中,使用了替换这个概念,我们可以分两层意义理解:
- 🍊是可替换的
- 它被替换成了🍐
考虑到,虽然这次🍊被替换成了🍐,下次它可能被替换成别的什么。于是我们约定一种记录:
数学家喜欢希腊字母作为标志,所以我们约定一种简化的记法:
λ\lambdaλ 读作lambda。这种记法也因此得名λ\lambdaλ表达式,或者lambda表达式。
在 λ\lambdaλ 之后,跟着一个符号,表示可替换内容的名字,它可以是🍊,也可以是随便什么东西。当然,最常见的情况,还是英文字母x。
再后面跟着一个点,点后面,就是等待被替换的内容了。
我们换一个更数学一点的例子:
这表示x+1中的x是可替换的。
到这里,我们不难看出,lambda表达式跟我们平时接触的“函数”,概念非常相似。
在lambda演算语境下,我们讲函数、lambda函数或者lambda表达式,都是一个意思。
lambda演算中,为了叙述方便,我们也可以给lambda表达式起名字,比如,可以定义:
当实际进行替换时,我们可以调用lambda表达式,我们可以直接调用lambda表达式,也可以用名字调用,写法是用一对圆括号括起来lambda表达式后跟一个空格,替换所需的内容,即参数跟在空格之后,例如:
其结果为:
当不产生歧义时,调用的小括号也可以省略。
邱奇的lambda演算正是这种最基本的替换思维出发,以此定义了整个数学世界。
邱奇数
如果我们要定义整个数学,那么在前面的例子中,就有严重的问题:我们并没有在lambda演算中定义出来🍌🍐是什么,在λx.x+1\lambda x.x+1λx.x+1中我们也没有定义加法和1。
所以我们前文提供的例子其实都不算真正的lambda函数。
略微思索,我们可以定义出一个合法的lambda函数:
这里我们仅仅使用了参数x,因为参数的意义在lambda演算中已经定义,所以是合法的。
在lambda演算中,因为不存在函数以外的数据类型,所以x必然也是一个函数。因此,我们可以把这个函数做一下改写:
注意,我们并没有创造一个新的lambda函数,我们只是改变了原有函数的写法,这个函数与前面的λx.x\lambda x.xλx.x是完全等效的。
因为此函数把f调用了一次,所以我们可以给他起名ONE。
接下来,我们试试能否创造更多的函数。
ONE把函数f调用了一次,那么,如果我们把f调用两次呢?于是,我们可以得到TWO:
那么,以次类推,如果我们把函数f调用三次、四次,那么:
现在我们有了对函数进行计数的函数,我们把它称作邱奇数。到此为止,邱奇数看起来并不像我们熟知的任何数学概念。
但邱奇数具有美妙的特点:它是被严格定义的,它所依赖的基本概念仅仅是“替换(lambda)”。
如果我们能够在接下来的推导中,找到它能够像我们小学学习的整数一样能够进行四则运算等操作,那么我们就可以用它替代没有严格定义的小学数学概念“自然数”、“整数”等等,进而构筑一个严格的数学体系的基石。
如果你认为λ\lambdaλ这样的数学符号看起来不够直观,也可以把它换成任何编程语言中的函数(需要语言支持闭包),这里提供一个JavaScript版。
const ONE = f => x => f(x);
const TWO = f => x => f(f(x));
const THREE = f => x => f(f(f(x));
const FOUR = f => x => f(f(f(f(x))));
邱奇数乘法
接下来我们考虑如何实现邱奇数乘法。邱奇数相乘的结果,应该是如下图所示:
根据邱奇数定义,n个f就是(n f)(n\ f)(n f),那么,我们只要再把它重复调用m次,上式可以写作:
于是,我们就得到了邱奇数乘法:
但是,先不要急,上面的式子并不是合法的lambda表达式写法。显然我们前面定义的lambda函数中并不支持多参数。所以我们没办法定义一个有两个参数的lambda函数。
那么,怎么办呢?
我们可以让 mul 函数先接受参数m,再返回一个接受n的新函数,接受完n之后,才真正得到计算结果。
这种技巧叫做柯里化(currying),注意,对于lambda演算而言,柯里化是一种技巧而非特性,它能够巧妙地利用闭包特性在仅支持单参数的环境中模拟出多参数的效果,可以说lambda演算是被迫使用柯里化的。
于是我们写作:
JavaScript版本:
const multiple = m => n => f => x => m(n(f))(x);
我们可以使用可运行的代码来验证邱奇数乘法,如:
//定义三个邱奇数
const TWO = f => x => f(f(x));
const THREE = f => x => f(f(f(x)));
const SIX = f => x => f(f(f(f(f(f(x))))));
//定义邱奇数乘法
const multiple = m => n => f => x => m(n(f))(x);
//任意定义f和x
let f = a => a + 2;
let x = 5;
//检验是否恒等
console.log(multiple(TWO)(THREE)(f)(x) === SIX(f)(x));
邱奇数加法
有了邱奇数乘法,我们思考一下,是否能实现其它四则运算呢?受到乘法启发,我们考虑加法的结果:
我们观察这个结果,发现我们首先求出 ((n f) x)((n\ f)\ x)((n f) x),再把它作为参数传入(m f)(m\ f)(m f)即可:
JavaScript版本:
const add = m => n => f => x => m(f)(n(f)(x));
我们同样可以验证邱奇数加法:
//定义三个邱奇数
const TWO = f => x => f(f(x));
const THREE = f => x => f(f(f(x)));
const SIX = f => x => f(f(f(f(f(f(x))))));
//定义邱奇数加法
const add = m => n => f => x => m(f)(n(f)(x));
//任意定义f和x
let f = a => a + 2;
let x = 5;
//检验是否恒等
console.log(add(THREE)(THREE)(f)(x) === SIX(f)(x));
邱奇数0
定义完加法和乘法,我们再来思考,是否存在邱奇数0呢?
从语义上来看,邱奇数表示不调用任何函数,所以它应该是:
可以验证,此邱奇数0对于邱奇数加法和乘法都是合理的。
小结
我们从朴素的"替换"概念出发,抽象出了lambda演算,并且迈向了定义整个数学的第一步,我们现在有了能够进行加法和乘法运算的邱奇数。
那么,接下来我们是否能够继续前进,定义出减法、除法,甚至分支、循环,直到形成一个图灵完备的系统呢?
留一个思考问题:假设m和n都是邱奇数,那么(m n)(m\ n)(m n)是哪种运算?
转载自:https://juejin.cn/post/7238917665851146296