引言
在计算机中,程序和数据并没有一个明显的分界线。数据可以被当作程序执行,程序也可以作为数据使用。
前者,很多语言提供了eval
功能,将一段数据作为代码执行,比如JavaScript
1 | eval("alert(1)") |
这里我们主要说一下后者。
假设有一门语言,它可以定义函数,调用函数,但没有提供基础的数字类型,我们要如何使用函数来实现数字这一特性。
为了实现这个特性,我们要对这个特性有个初步的了解。
也就是,什么是数字?
在日常生活中,我们可以说,我有3个苹果、他打了我2下、口袋里空空如也1块钱也没有等等。
我们把数字作为计数的一个东西(循环定义?),换句话说是某一个东西(上述苹果)或某个动作(打人)的重复。
3个苹果就是有一个苹果,又有一个苹果,还有一个苹果;
打人2下,就是打一下,再打一下;
0就是空空如也,口袋没钱或者没执行打人这个动作。
如果一种语言可以定义函数、调用函数,那我们就可以通过定义一个函数,然后再调用这个函数表示数字,调用这个函数n次就代表数字n。
这种程序表示数字的方式叫邱奇编码,数字叫邱奇数。以lambda演算的发明者阿隆佐·邱奇命名。
定义数字
邱奇数就是函数被调用的次数。
为了使函数可以被调用多次,我们让这个函数调用的返回值能够被重新应用于这个函数。
换句话说,这个函数的参数和返回值的类型是相同的。
比如定义函数f,函数参数为a
那1就是f(a)
,2就是f(f(a))
,以此类推。
对于0,就是函数f,一次也没被调用。
没被调用怎么表示?
我们需要注意的是,数字是函数被调用的次数,在数字被使用前,函数是不能被调用的,否则调用完了数据也就看不到了。
有点像上发条的玩具,定义一个数字就是给玩具上好适当的发条,使用这个数字时,就是打开开关,玩这个玩具看被上了多少发条。
也就是说1,实际上是——接受一个函数和此函数的参数,并将这个参数应用到这个函数上。是一个动作,也是一个函数。
1 | one = \f a -> f a |
另外一种语言,比如js写就是
1 | var one = function(f, a) { |
既然如此,0就是
1 | zero = \f a -> a |
传进的函数f,被忽略
简单计算
数字是可以被计算的,如两个数字相加
先从简单的开始,加一个固定的数,如加1
按照之前的定义,加1,就是在原来的基础上,多执行一次相同的动作
比如:
1 | addOne n f a = f (n f a) |
为什么这样写?为什么会有三个参数,这分别代表什么意思?
在分析之前,我们先考虑下类型,邱奇数的类型、作为依据的被调用函数的类型以及计算函数参数的类型等
首先看被调用函数的类型
之前提到
为了使函数可以被调用多次,我们让这个函数调用的返回值能够被重新应用于这个函数。
换句话说,这个函数的参数和返回值的类型是相同的。
这里包含以下信息:
- 这个被调用函数是一个函数(显而易见)
- 这个函数可以接受一个参数
- 这个函数的参数类型与返回值类型相同,返回值可以被重新应用于此函数
也就是我们可以给此函数的类型起个别名,用以下表示
1 | -- 接受一个类型a,返回一个类型a,别名Func |
再看数字的类型
之前也有提到
1,实际上是——接受一个函数和此函数的参数,并将这个参数应用到这个函数上。是一个动作,也是一个函数。
同样,这包含以下信息:
- 这个邱奇数是一个函数
- 这个函数接受两个参数,一个是之前提到的被调用Func a,一个是可被应用到函数的参数,也就是a
- 考虑这个函数的返回值类型,由于函数体只是简单的将参数应用到被调用函数上,然后返回,所以其类型也就是Func a的返回值类型,同样是a
于是,我们就有了邱奇数的类型别名
1 | -- 接受一个类型Func a和类型a,返回一个类型a,别名ChurchNum |
对于加一函数addOne
很明显,它接受一个类型ChurchNum a,返回另一个比它大1的另一个ChurchNum a
也就是
1 | addOne :: ChurchNum a -> ChurchNum a |
如果返回值类型不用邱奇数的类型别名,则为
1 | addOne :: ChurchNum a -> (a -> a) -> a -> a |
那addOne的实现
1 | addOne :: ChurchNum a -> (a -> a) -> a -> a |
可以理解为,addOne接受一个ChurchNum a类型的邱奇数,实参为n,返回另一个邱奇数,这个返回的邱奇数(类型为函数),接受两个参数,实参分别为f和a
到这里,两个邱奇数相加的函数add,也就很容易出来了
1 | -- 类型,接受两个邱奇数,返回另一个邱奇数 |
检验
我们一直在说邱奇数是一组动作,这组动作代表一个参数被应用于一个函数多少次
还举例说定义数字像是给玩具上发条,使用数字是让玩具动起来
那怎样才能让玩具动起来
我们可以用实际的例子来说明
比如我们可以说动作是——给一个字符串头上加一个字符'a'
,动作的参数就是被加的字符串,初始参数这里我们用空字符串
1 | let f = ('a':) |
做几个简单的计算
1 | -- 0,执行这个函数,看看结果,也就是让玩具动起来 |
虽然我们之前假设系统中没有数字类型,但为了方便我们的验证,用其做一下检查是没问题的
我们可以说动作是——给一个系统数字加一,动作的初始参数是0
运行
1 | let f = (+1) |
得到
1 | Prelude> let f = (+1) |