为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Ruby

之前有发布一篇文章《用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的基础上调用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 61 [ 2 3 ] 4 5 61 2 [ 3 4 ] 5 61 2 3 [ 4 5 ] 61 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

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

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

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

Happy Coding!!



作者:lanzhiheng
链接:https://www.jianshu.com/p/9ed54b561563

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消