之前有发布一篇文章《用Ruby简单模拟Lambda演算》讨论了如何用Ruby的Lambda来表达0, 1, 2这些整数。专业上称他们为邱奇数,顺手贴一段Wiki。
邱奇编码是把数据和运算符嵌入到Lambda演算内的一种方式,最常见的形式是邱奇数,它是使用Lambda符号的自然数的表示法。这种方法得名于阿隆佐·邱奇,他首先以这种方法把数据编码到Lambda演算中。
但如果要构建一个程序,仅仅有这些基本的数据类型是不够的。很多时候我们会想对我们生成的这些数字做些猥琐的操作。比如我们常用的加法,减法,乘法,以及阶乘等等。下面我们就来实现这些操作。
0. 热身
古人云:“温故而知新,可以为师矣”。那么开始实现基本数值运算之前我们先来温习一下之前的知识,顺手实现几个邱奇数
ZERO = -> p { -> x { x } }
ONE = -> p { -> x { p[x] } }
然后写一个函数把对应的邱奇数以我们熟悉的方式展示出来
def to_integer(n)
n[-> x {x + 1}][0]
end
>> to_integer(ONE)
=> 1
>> to_integer(ZERO)
=> 0
更多的邱奇数
TWO = -> p { -> x { p[p[x]] } }
THREE = -> p { -> x { p[p[p[x]]] } }
FOUR = -> p { -> x { p[p[p[p[x]]]] } }
# ....
只要反复调用p
我们便能够构建更大的邱奇数,那么我们是否能够创建一个函数来完成这个反复调用p
的过程呢?我期待的函数是这样工作的
F(ONE) => TWO
F(TWO) => THREE
F(THREE) => FOUR
似乎有点意思,那这里我姑且命名这个函数为SUCC
。
SUCC = -> n {
-> p {
-> x {
p[n[p][x]]
}
}
}
讲解: 简单地可以把这个函数拆开来看,我们在
x
的基础上调用n
次p
就会得到我们期待的数字n
。当我们在得到结果的基础上再执行一次p
的时候就会得到n + 1
了。
>> to_integer(SUCC[ZERO])
=> 1
>> to_integer(SUCC[ONE])
=> 2
OK,结果正是我们所期待的。 但好像热身热过头了,那我们马上开始用Lambda实现基本数值运算。
1. 加法
热身里面好像隐藏了一些东西,我们平时所定义的一个数字,其本质就是在0
的基础上执行 加一
操作得到的,我们想要什么数字就执行多少次加一
操作,而这个时候我们的起始值是0
。那么如果起始值不是0
呢?那会发生什么事情?
这不就是加法的本质吗?我们把两个数m,n相加, 其实就相当于在m
的基础上做了n
次加一
操作罢了。只是这个过程太平常了,平常到我们几乎意识不到它的存在。
那现在就要把代码写下来了, 可以复用我们热身的时候定义的SUCC
函数,写下如下代码
ADD = -> n {
-> m {
n[SUCC][m]
}
}
讲解: 我们用到了我们已经实现的
SUCC
函数,从字面上来理解就是我们从基础数m
开始,不断递增自身,当递增n
次的时候我们就能够得到n + m
的结果了。如果拆开来看会更加明显,我们当n
为THREE
,m
为TWO
的时候,代入函数,便可得到这样一个表达式SUCC[SUCC[SUCC[TWO]]]
,其实就是把TWO
递增3次,最后会得到FIVE
。
验证一下结果是否正确
>> to_integer(ADD[FOUR][FIVE])
9
>> to_integer(ADD[FIVE][ONE])
6
结果还是另人满意的。
2. 乘法
实现了加法之后,似乎乘法已经离我们不远了。我们回想一下最初学习乘法的时候,小学老师肯定不是一上来就讲乘法的。一般他们都会列出这样一条式子m + m + m + m + .....
,就是一个数m
加上自身n
次,这就是乘法的本质了。然后他们会把这个式子表示成m x n
。那既然我们已经实现了加法了,是否可以利用我们已经实现的ADD
函数来实现乘法呢?
按照乘法的原理来实现MULTI
函数
MULTI = -> n {
-> m {
n[ADD[m]][ZERO]
}
}
讲解: 从字面上来理解就是我们要从基础数
ZERO
开始,累加m
这个数n
次。拆开来看会更加直观,我们假设m
为TWO
,n
为THREE
的时候,会转化为ADD[TWO][ADD[TWO][ADD[TWO][ZERO]]]
。
检验一下结果
>> p to_integer(MULTI[FOUR][FIVE])
20
>> p to_integer(MULTI[ONE][FIVE])
5
3. 减法
加法和乘法似乎都不会很难,但是减法就有点麻烦了。我们前面都感受到加法的本质就是一个不断重复调用指定函数的过程。但减法应该是一个同他相反的过程,这该怎么做啊?调用方法我们还知道该怎么调,但是我们没有办法把已经调用了的方法回退,让它返回前一次调用的结果。因此,这个作者才会说Lambda Calculus: Subtraction is hard。咋整?下面先介绍一下我们需要用到的辅助函数。
1) 有序对
Wiki上面对有序对的解释
在数学中,有序对是两个对象的搜集,使得可以区分出其中一个是“第一个元素”而另一个是“第二个元素”(第一个元素和第二个元素也叫做左投影和右投影)。带有第一个元素a和第二个元素b的有序对通常写为(a, b)。
我们尝试用Lambda表达式来实现这个有序对
PAIR = -> first {
-> second {
-> p {
p[first][second]
}
}
}
LEFT -> first {
-> second {
first
}
}
RIGHT -> first {
-> second {
second
}
}
我们定制了一个PAIR
,它接受3个参数,分别是两个邱奇数以及一个访问函数,访问函数就是我们的LEFT
, RIGHT
分别用来提取PAIR
中的两个邱奇数。验证一下这个PAIR
是否能够像我期望那样去运行?
>> p to_integer(PAIR[TWO][FIVE][LEFT])
2
>> p to_integer(PAIR[TWO][FIVE][RIGHT])
5
工作正常。接下来我们看看滑动窗口。
2) 滑动窗口
计算机网络里面有过这样一个概念。形式上理解就是
[ 1 2 ] 3 4 5 6
1 [ 2 3 ] 4 5 6
1 2 [ 3 4 ] 5 6
1 2 3 [ 4 5 ] 6
1 2 3 4 [ 5 6 ]
这能看出什么?在中括号括起来的地方不就是我们有序对吗?上面这个滑动窗口,其实我们可以用有序对来模拟。当初始值设置为(0, 0)的时候滑动3次就会有如下效果
[0, 0]
[0, 1]
[1, 2]
[2, 3]
有点像是我们之前定义的SUCC
方法,只是我们那时候只操作一个邱奇数,返回一个邱奇数。而在这里我们是传入一个PAIR
, 然后返回这个PAIR
向右滑动的结果。
那我们尝试编写这个函数
SLIDE = -> p {
PAIR[p[RIGHT]][SUCC[p[RIGHT]]]
}
讲解: 该函数接受一个
PAIR
作为参数,然后创建一个新的PAIR
,以原来PAIR
的右边的元素作为新PAIR
的左边的元素。而新PAIR
右边的元素用的是原来PAIR
右边元素递增之后的值。
在验证这个方法正确性之前我还需要实现to_pair
这个方法,用来方便我们对PAIR
内容的检查
def to_pair(pair)
"(#{to_integer(pair[LEFT])}, #{to_integer(pair[RIGHT])})"
end
>> puts to_pair(PAIR[ONE][TWO])
(1, 2)
>> puts to_pair(SLIDE[PAIR[ONE][TWO]])
(2, 3)
>> puts to_pair(SLIDE[SLIDE[PAIR[ONE][TWO]]])
(3, 4)
哈哈,SLIDE
的行为已经跟我们的预期一样了。
3) 实现减法
说了一大堆,并且实现了PAIR
, SLIDE
。但是我们还是没有解决问题。我们通过PAIR
以及SLIDE
的配合可以获取到当前的邱奇数还有它的前驱邱奇数。那么接下来怎么搞?我们换个角度想想这个问题,我们现在已经能够构造一个PAIR
以及SLIDE
了,那么我们是否可以实现一个函数,让它能够直接返回我们传入某个邱奇数 的前一个邱奇数?如果我们能够实现这样一个函数PRED
,那么当我对一个数m
重复调用PRED
这个函数n
次的时候,不就可以获取到m - n
的值了吗?(当然我们的前提是 m >= n毕竟我们现在还没有考虑到负数)
有了前面的基础PRED
方法似乎比较好实现
PRED = -> n {
n[SLIDE][PAIR[ZERO][ZERO]][LEFT]
}
讲解: 当我们传入的邱奇数为
n
的时候,我们对最基本的有序对(0, 0)
向右滑动n
次,然后就能得到新的有序对(n - 1, n)
,我们取有序对左边作为结果就能够得到n-1
检验一下PRED
的执行情况
>> p to_integer(PRED[FOUR])
3
>> p to_integer(PRED[TWO])
1
>> p to_integer(PRED[ONE])
0
Cool,已经可以获取到指定邱奇数的前一个邱奇数了。接下来干嘛呢?好像减法的实现已经呼之欲出了。其实我们实现的PRED
就是一个减一
操作,如果我们想要得到m - n
的结果,那么只需要对m
执行减一
操作n
次即可达到目的。
SUBT = -> m {
-> n {
n[PRED][m]
}
}
检验结果
>> p to_integer(SUBT[FIVE][ONE])
4
>> p to_integer(SUBT[FIVE][TWO])
3
>> p to_integer(SUBT[THREE][TWO]
1
我去,总算大功告成。相比起加法还有乘法,要用Lambda来实现减法居然要耗费这么多力气。最后简单总结一下减法的思路: 首先我们需要设计一种数据结构PAIR
用来存储两个邱奇数。然后我们实现一个SLIDE
让我们可以从一个基本的邱奇数有序对PAIR[ZERO][ZERO]
开始滑动,当我们滑动n
次的时候就会得到n
的邱奇数以及n-1
的邱奇数。利用这两个函数我们可以实现一个PRED
函数,通过这个函数我们可以直接获取到我们作为参数的邱奇数的前一个邱奇数 - 1。最后我们只需要对一个数m
反复调用PRED
函数n
次就能够得到我们期望的m - n
。把这个过程封装一下之后就能够得到我们期望减法SUBT
函数了。
4. 阶乘
最后我们讨论一下阶乘,其实搞懂了前面这些之后,阶乘实现起来就非常简单了。我们知道乘法m x n
就是m
从0开始累加n
次m
。那么阶乘呢?m ^ n
其实就是从1开始连续乘n
次m
。原理知道了,代码其实也就很好写了。
POWER = -> m {
-> n {
n[MULTI[m]][ONE]
}
}
最后检验运算结果
>> p to_integer(POWER[FIVE][FIVE])
3125
>> p to_integer(POWER[FOUR][FOUR])
256
>> p to_integer(POWER[FOUR][ONE])
4
5. 尾声
用《我编程,我快乐》里面的一则建议来说就是:“如果你想学懂某一种技术,就把它教给别人吧。”相关的代码片段已经贴到了gist。
不好意思,又一口气啰嗦了一大堆,分别讲了加法,减法,乘法, 阶乘这些运算的Lambda实现方式。如果你觉得作者表达得不够清晰的话,还可以参考一下这些文章。
Introduction to Lambda Calculus: 简单地讲解了Lambda实现基本数值运算的基础知识。
Lambda Calculus: Subtraction is hard: 详细讲解了如何用Lambda演算来实现减法。
Lambda 演算Wiki:简单讲解Lambda演算的一些基础知识。
私人推荐书籍
《我编程,我快乐》:一本关于编程还有就业的好书,正在读第二遍感觉很值得一读。