用Ruby的Lambda实现基本数值运算

之前有发布一篇文章《用Ruby简单模拟Lambda演算》讨论了如何用Ruby的Lambda来表达0, 1, 2这些整数。专业上称他们为邱奇数,顺手贴一段Wiki

邱奇编码是把数据和运算符嵌入到Lambda演算内的一种方式,最常见的形式是邱奇数,它是使用Lambda符号的自然数的表示法。这种方法得名于阿隆佐·邱奇,他首先以这种方法把数据编码到Lambda演算中。

Calculator

但如果要构建一个程序,仅仅有这些基本的数据类型是不够的。很多时候我们会想对我们生成的这些数字做些猥琐的操作。比如我们常用的加法,减法,乘法,以及阶乘等等。下面我们就来实现这些操作。

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的基础上调用np就会得到我们期待的数字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的结果了。如果拆开来看会更加明显,我们当nTHREEmTWO的时候,代入函数,便可得到这样一个表达式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次。拆开来看会更加直观,我们假设mTWOnTHREE的时候,会转化为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开始累加nm。那么阶乘呢?m ^ n其实就是从1开始连续乘nm原理知道了,代码其实也就很好写了。

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

Step by Step

不好意思,又一口气啰嗦了一大堆,分别讲了加法减法乘法阶乘这些运算的Lambda实现方式。如果你觉得作者表达得不够清晰的话,还可以参考一下这些文章。

Introduction to Lambda Calculus: 简单地讲解了Lambda实现基本数值运算的基础知识。
Lambda Calculus: Subtraction is hard: 详细讲解了如何用Lambda演算来实现减法。
Lambda 演算Wiki:简单讲解Lambda演算的一些基础知识。

私人推荐书籍
《我编程,我快乐》:一本关于编程还有就业的好书,正在读第二遍感觉很值得一读。

Happy Coding!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容