*写这篇文章的背景是最近在计算语义学的操作语义,指称语义和公理语义,由此联想到一些关于计算本质的思考。
一、我们如何理解这个世界?
(1)混沌世界的简化
世界由一些基本的元素组成,这里是指抽象的基本元素(我当然无意于
争论宇宙是否有真正的基本粒子)。然后自然而然地,这些元素在空间和时
间维度上发生着联系,通常
我们习惯上把空间上的联系叫做联系或者关系,如A和B相距100米
我们习惯上把时间上的联系叫做变化或者演化,如A变成了B
(我们不关系具体的时间,而更加在乎先后关系的成立)
如果我们将空间中的元素和时间维度上以后可能变化成的元素放到一起,
那么我们就得到了所有的元素,于是对于我们而言,这个世界就只剩下了三
个概念:
元素
联系
变化
(2)序偶、关系、映射
于是我们到了集合论的初期,联系在集合论中更加倾向于称为序偶,就是
更多的时候,我们比较关心有规律的联系,或者说有相同性质的联系。如
果有一组序偶,他们具有某些相同的性质,我们可以将这组序偶集合定义为关
系。为了研究关系的性质,我们定义了关系的自反、对称、传递、相容、等价
性质。由这些性质出发,定义了关系的自反闭包、对称闭包、传递闭包、相容
闭包、等价闭包等等。
而对于变化,我们可以用映射这个概念来描述,a->b表示a到b这个变化。
注意,最初的变化概念,是允许分裂概念的,如a->b,a->c同时存在。当然也允
许合并的概念,如a->c和b->c同时存在。变化左边我们成为原像,变化右边我们
成为像。如果不允许分裂概念,就是单值映射,如果不允许合并概念,就是单根
映射。由此我们可以推出一一映射这样的概念。
(3)越来越简单的变化
我们继续前进,我们发现分裂是让事物变得越来越复杂的变化,而合并是让
事物变得越来越简单的变化,一一映射我们可以粗略认为复杂度没有变化。我们
总是希望化繁为简,所以我们选择了一一映射以及合并概念,组合成一种我们认
为是在简化事物的变化。我们称之为函数,允许合并,不允许分裂。
f(2)=3 ,f(3)=3 同时成立可以
f(2)=2 ,f(2)=3 同时成立不可以
(4)用变化定义静止
什么是静止?静止是一种一直不动的变化。
二、尝试用函数来理解世界
(1)lambda函数为什么叫做匿名函数?
如果将世界上的一切变化都理解为函数,那么有没有一种标准的方式来描述
函数?让我们看看下面的三个函数
f(x) = x+1
g(x) = x+1
h(y) = y+1
这些有什么不同吗?似乎它们本质上是一样的。我们应该能迅速得到一个观念,
函数的本质不同来自于函数所完成的功能或者计算,
1. 与函数的名字无关
2. 与函数的自变量名字无关
为此我们引入2个基本概念,来解决一下这个问题
1. 匿名函数,所有函数的名字都叫lambda,或者用希腊字母λ表示
λ(x) = x+1
或者写得更加简单点儿: λ x.x+1,表示输入变量是x,返回的结果应该是x+1
2. 给出变量的转换规则
λ x.x+1 和 λ y.y+1 等价
(2)尝试将元素本身也定义成函数
如果我们想要将所有变化归结为函数,那么基本的常数呢?1,2,3,4,5
还记得皮亚诺系统吗?
0 定义为 Ø
1 定义为 {Ø}
2 定义为 {Ø,{Ø}}
3 定义为 {Ø,{Ø},{Ø,{Ø}}}
后继操作,也就是++操作定义为
x++ => x∪{x}
加法操作,定义x+n为
在x上执行n次++操作
那么这个用空集定义的系统至少在自然数加法这个方面表现得普通自然数没
有区别了,实际两个系统相对于加法而言是同构的。
当然,我们可以对lambda演算做同样的映射工作:
0 定义为 λf.λx.x 也就是无论输入什么参数,都是返回λx.x这样一个函数
1 定义为 λf.λx.f x 也就是对输入的x,使用1遍f
2 定义为 λf.λx.f (f x) 也就是对输入的x,使用2遍f
3 定义为 λf.λx.f (f (f x)) 也就是对输入的x,使用2遍f
++操作定义为:
λn.λf.λx.f (n f x)
例如++0
λn.λf.λx.f(n f x) λf.λx.x -> λf.λx.f (λf.λx.x f x)
-> λf.λx.f (λx.x x)
-> λf.λx.f x
那0++呢?
λf.λx.x λn.λf.λx.f(n f x) -> λx.x不适用于后加操作
看上去,好像很繁琐的样子,但是我们细心一点儿,注意一下 (n f x)这个表达式,
对于我们刚刚定义的自然数,每次都会得到自己,那么λn.λf.λx.f (n f x)的本质就是
在n的前面添加一次f应用。
加法操作定义为:
λx.λy.x++y
还记得x的定义吗?n本质上是一个函数,接受2个参数f和x,对x使用n次f。那么加
法操作的定义,可以理解为接受2个参数m和n,对n使用m次++操作。
另外逻辑变量和操作可以定义为函数
true = λx.λy.x 输入2个变量,总是返回第一个变量
false = λx.λy.y 输入2个变量,总是返回第二个变量
if = λv.λt.λf.v t f 输入3个变量,根据第一个变量的值,
决定是返回第一个变量还是第二个变量
第一个变量v必须是true或者false
事实上我们可以定义出整数集合上的所有基础算术运算,布尔值集合上基础逻辑运算。那
么浮点呢?按照IEEE754标准,浮点也就是一个<符号,阶码,尾数>组成的有序对,本质就是
3个不同整数的运算。那么实数呢?抱歉,计算机本质上就是整数的计算机,计算机只能近似地
处理实数。
(3)我们遭遇了递归:
我们现在相信我们已经有了一个整数运算系统和布尔逻辑系统。但是这足以表述我们所说
的元素间变化吗?我们还差最后一环,函数的自我调用,也就是递归。我们为什么需要递归?函
数的本质就是描述一种有规律的变化。
思考一下如下数列:
1,2,3,4,5,6,7,8
我们如何描述它:
a(n) = a(n-1)+1
那么更加复杂的变化呢:
1,1,2,3,5,8
好的,你认出来了,这是斐波那契数列。
我们可以更加复杂:
f(n)= f(n-1)*f(n-1)+f(n-2)*6+n^2
以及一些基本不可能求出通项公式的数列变化,描述这些变化的方式基本只能是递归的
定义,或者说我们只有知道了前面的变化结果后,才能知道下一步该怎么计算。
有人还会问,我们是否需要循环?我们已经有了分支选择(前面的if还记得吗),我们
如果再有循环,很多人基本上就会相信这个模型是图灵完备的了。因为,确实很多程序语言的基
本控制结构就是分支和循环。这里只简单提一下,循环可以转化为递归的一种特殊简化形式--尾
递归。举一个简单的例子,计算arr[n]的元素之和
for (int i=0; i
sum += arr[i];
}
可以转化为:
getSum(arr, sum=0, k=0) {
if (k==n) {
return sum;
} else {
return getSum(arr,sum+arr[k],k+1);
}
}
好吧,看来我们真的需要函数自己能够调用自己了。
(4)我们如何调用自己
我们在定义一个函数的时候,第一这个函数是没有名字的,第二自己的定义还没有完成,
我们如何调用连自己的定义还没完成的没有名字的函数呢?你写下了λf.那么函数体怎么写
来调用自己?
直接调用看来是不现实了,自己都还没定义完,调毛线的自己啊!那么间接调用呢?来
看看一个神奇的函数,Y combinator:
Y = λf.(λx.f (x x)) (λx.f (x x))
将任意函数g作为参数传入Y
Y g = λf.(λx.f (x x)) (λx.f (x x)) g
= (λx.g (x x)) (λx.g(x x))
= (λy.g (y y)) (λx.g(x x))
= g (λx.g(x x) λx.g(x x))
= g (Y g)
我们用阶乘函数作为例子,来看看Y combinator的工作机制:
f(n) = if n=0 then 1 else n * f(n-1)
可以用前面说的方法定义出 ISZERO 和 MULT
ISZERO v x y : v为ture时返回x,否则返回y
MULT x y : 返回x*y的结果
我先偷一下懒吧,大家也做做简单的练习吧。于是我们可以把上述阶乘函数定义为:
F = λf.λx. (ISZERO x) 1 (MULT x (f (x-1))
这里添加了一个函数参数f,用于接收自己,也就是为了使用这个阶乘函数,我们还
需要传入一个函数作为参数,那么这个参数传什么呢?只需计算Y F = F (Y F),
这是一个新的函数,这个函数的特点就是已经有一个Y F作为参数了,只需要一个数
值参数就可以进行运算了,我们再来看看具体的Y F的表达式
Y F = F (Y F)
= λx. (ISZERO x) 1 (MULT x (Y F (x-1)))
我们将Y F记作g,则
g = λx. (ISZERO x) 1 (MULT x (g (x-1)))
这个函数就只需要一个参数x了,为了避免你说我还是用了名字函数g来表示自己,
我将g换成匿名函数的形式,将g函数表示为Y F的形式,也就是
λf.(λx.f (x x)) (λx.f (x x))
λf.λx. (ISZERO x) 1 (MULT x (f (x-1))
由于实在比较长,我分了2行写,那么原来的g就定义为:
λx. (ISZERO x) 1
(MULT x (
λf.(λx.f (x x)) (λx.f (x x))
λf.λx. (ISZERO x) 1 (MULT x (f (x-1))
(x-1)))
这下没话说了吧,这是一个完全符合lambda演算定义,
接受1个数值作为参数,完成阶乘计算的函数。
现在我们应该可以对lambda计算有个直观的概念。其实用函数来描述计算的本质是众多思潮
中的一种。人们其实一直对什么是逻辑的本质?什么是计算的本质?什么是数学的本质?这类问题
充满了好奇心。而对计算的本质,或者说程序的本质,人们应该尝试了从3个角度来理解。
1. 构造1种抽象的机器,比如说无限内存,不考虑寄存器,所有操作直接针对内存。并定义
一组在这个抽象机器上的操作。图灵机,程序语义学中的操作语义,都是这种思路来理解
计算的本质。
2. 通过定义函数,认为世间变化无外乎函数,计算机有输入,产生输出,本质上就是一个函
数,只不过这个函数比较复杂而已。更进一步,我们把参与运算的元素本身也看做是函数,
正如前面我们将自然数定义成lambda函数一样。
3. 通过逻辑表达式,认为程序就是逻辑推导。目前理解不多,就不多得置评了。