程序语言的自我意识与仿他意识

注:本篇博文为重发稿件,“CSE脚本世界”曾被清空过,现已找回原文。


这本写于两年前(注:指2006年),网上早就盛传开来,当然,大部分转载都把我的大号隐掉了,标为“来源:互联网,作者:佚名”。这里重发一下,一是因为当时写这篇文章确实花了些时间,还修订过几次,二是为了正本清源,本文乃Wayne chan所作,有人想转载请标明出处。

一年前有人将本文推荐给某大学的学术期刊,有编辑找到我,问我想不想在他那里发表,我寻思,愿发就发吧,本人不图稿费,也乐于为人捧场。然后这个扯着嘶哑口音的编辑给我指出一些文字上要改进的地方,我一一记下来了,末了,老先生告诉我,如果想发表,请交xx元费用。我的热心劲一下子消失了,礼貌性回绝了他的要求,这世道怎么老跟钱挂钩,不要稿费也就罢了,怎么还拉赞助呢?他们期刊的质量想必好不到哪儿去。


程序语言的自我意识与仿他意识

2006-4-8, by wayne chan

 

从浮点数说起

昨天有一位网友问我一个问题,为什么表达式“1.9 + 2.3 == 4.2”在Python中计算结果是FALSE?我回答说,计算机模拟浮点数时会失真,网友说这个他明白,但为什么“1.9 == 1.9”结果却是TRUE,机器看1.9也同样失真呀?实际上,两个表达式是有差别的,前者在比较之前运算了,而后者没有。

这是一个很有趣的问题,如果我们把电脑看成有感知的生物,上述问题反映了人脑与电脑是按不同方式认识事物的。首先,电脑总是以2进制表达数值的,而人脑习惯使用10进制,比方被人脑标识为0.125的数据,到电脑中就被看作0.001,即“0/2 + 0/4 + 1/8”。这两种方式都能唯一表达浮点值,本例中,两个浮点值能在10进制与2进制之间顺利转换,但不幸的是,大部分10进制浮点数无法准确的转化成2进制,比如上面的1.9,在Python程序中,它显示为1.8999999999999999。当然,这个数值还不是电脑自己认为的值,它经过两次转换,第一次是我输入1.9后,电脑把它转换成它认识的2进制格式,第二次是电脑要告诉我它接收到的值是多少,又把2进制数据转化成10进制打印出来。

其次,电脑总尝试以最精确的形式表达一个数据,而人脑往往以简单形式表达复杂数据。比方,老板告诉你,“好好干活,这事成了分你三分之一利润”,如果老板长一个CPU脑袋,又很有偏执狂精神,他可能告诉你,“好好干活,这事成了分你0.3333333333333333...”,幸好本人还不太偏执,否则,我一辈子不用干别的,把这句话转述完就安息吧。

在Python中计算“1.0 / 3”得到结果值是0.33333333333333331,这是32位电脑对现实认知的最精确形式,而我们拿“1/3”来表达,为什么既省事,又精确?这里“1/3”不只是数值,还隐含了一种计算:先3等分再取走1份。之所以精确,因为它是自描述的,我们还原了它的本来面目,就像宋玉在《好色赋》中形容东家美女,“增之一分则太长,减之一分则太短”,如果提高浮点精度,不论32位、64位,还是128位CPU,存贮1/3最终都要截除尾数,都无可避免的要失真。

我们先确立两个概念,其一,某种形式化的计算过程,等价于静态数据;其二,自我描述的数据可以不失真。

表达式是不是数据?

1/3是自描述的数据,同时它还是一个可计算的表达式,我们推而广之,是不是所有表达式都是数据?这是一个见仁见智的问题,应该没有标准答案。

应用程序本来就是对现实世界一种模仿,编程语言只是实施模仿的辅助手段,要不要把表达式看成数据,完全在乎你——编程主体,以及当前依赖的环境——开发工具,需要采用哪种方式会更好达成目标。这句话有两层含义,首先,编程环境可以影响一个表达式该不该成为数据,比如,在C语言中,表达式是表达式,它最终编译成运行代码,数据是数据,编程中可直接读写。而在LISP语言中,表达式也是数据,既用于运算,也支持直接读写。此外第二层含义,即使开发语言很大程度上决定表达式该不该成为数据,却不绝对,还与应用相关,比如某些防跟踪软件,即使用C语言开发,也局部的将运行代码看成数据,运行中动态修改,让它在反编译工具变成花指令。

表达式是不是数据,是函数式编程(Functional Programing,FP)区别于命令式编程(Imperative Programming)的重要差异,也是根本性差异。按严格定义,函数式编程把程序的输出定义为其输入的一个数学函数,只有把函数调用也看成数据,中参数中层层串接,最后才组装出完整的程序。比如:if..else句式,使用函数式语言表达如下:

else(if(expr,TRUE_branch),FALSE_branch);

外层调用else函数时,先计算第一个参数(即if函数调用),如果if条件成立,执行TRUE_branch语句并返回TRUE,否则直接返回FALSE,接着else函数分析if调用的返回值,决定FALSE_branch语句是否要执行。这个例子我们看到,把函数用作参数,可以连接多个运算。

按严格FP定义,函数式编程没有内部状态的概念,而命令式编程要频繁使用赋值语句,用变量保存内部运行状态,比方if..else在命令式语言(如C语言)中实现时,两条语句看似无关,但实际是按固定格式框定了的,前后必定相连,计算中系统还隐式的使用临时变量记录if条件值。

我们常见的语言,包括C、C++、Pascal、Java等都是函数式语言,函数式语言主要有LISP、Haskell、Scheme、ML等,除了这两者,还有一类逻辑式编程(Logic Programming,LP)文献中通常也将它看成独立分支。因为,大部分函数式语言都很容易构造出逻辑式程序,LP与FP实际是比较近似的。

所以,大致而言,命令式与函数式代表当前编程语言的两大流派,本文尝试从根源上探究两者差异的原因,借用摩尔根(美国生物学家,1866—1945)的话,基因决定性状,我们尝试从基因分析入手,分析两者内在机制的差异性,理出脉络后,再将他们的外在差异贯穿起来讲述。

高阶函数与惰性求值

高阶函数(High-Order function)与惰性求值(Lazy evaluation)是函数式编程的重要特征,这两者都直接起源于运算式的形式化描述,也直接促使FP具有广泛的多态性。

所谓高阶函数是指以函数作为参数或返回值是函数的函数,比如Python中的apply函数:

apply(pow,[1.9,2])
8

pow是Python一个内嵌函数,把它当作一个参数传给apply,apply函数运行中,先把pow及两个参数组织成可计算对象(即Python中CodeType类型的数据),然后完成pow(1.9,2)求平方数运算。

下面,我们再看看惰性求值是怎么回事,比如我们定义一个函数handover,它的功能是把一个表达式从一台机器传递到另一台机器再计算,如:

handover(pow,[1.9,2],’Machine_B’)

惰性求值可以让我们在需要的时候才做计算,特定场合下惰性求值能产生奇异功能。比如,本例类似于前面提到1/3,我们知道,在异构系统中传递数值会失真,如果我们把“1.9 ** 2”这种自描述的数据传递给更高精度的CPU,比起算出浮点数再传递,结果肯定更加精确。再如,某些树状结构下实现快速搜索,遍历树节点时我们附带算法(当作数据被传递),在特定节点或特定情况下,才将它看成表达式进行计算。
让程序操纵它自身代码,将形成比面向对象语言更为广泛的多态性,不只程序的伸缩性增强了,也让软件具备某种自我感知的能力。

比如,1971年鲍布.托马斯编写了一个叫“爬行者”的程序,这个程序能自我复制,能从网络一个节点爬到另一个节点,还出乎意料的在每一台机器的屏幕写上“I’m creeper! Catch me if you can!”。没过多久,整个局域网就布满了爬行者,它们消耗很多资源。为了清理这些淘气包,另一个程序员设计出一个叫“收割者”的程序,它可以搜寻并删除爬行者,当系统中再也找不到爬行者时,收割者就执行程序中最后一项指令:毁灭自己,然后从电脑中消失。

这种爬行者与收割者实际上是病毒软件与反病毒软件的雏形,把自身代码也当成数据来处理,就轻松实现自我繁殖。当然,自我繁殖还只是一种简单的复制拷贝,后面章节我们将进一步讨论程序语言的更高形态——自我演进。


二元世界

下面再看另一个函数式语言特征:lambda演算。形式上我们可以把lambda看作动态定义的匿名函数,比如在Python中:

>>> apply(lambda x: x * x, [3])
9
>>> iValue = 4
4
>>> apply(lambda x: x * iValue, [3])
12

lambda运算深刻的表现出FP独有特征,实际上,lambda演算先于函数式语言就存在了,1930年Alonzo Church和Stephen Kleene为推论构造性证明与非构造性证明,建立起lambda数学基础,后来lambda引入LISP等语言,成为函数式编程的基础。

lambda演算克服了命令式语言的非构造性缺陷,也即,运算过程没有告诉我们,如何获得“针对任一输入得到确定输出”的计算方法。所谓构造性证明是指能具体的给出某对象,或某对象的计算方法,当能证实“存在一个X满足性质A”的证明时,我们称它是构造性的,反之,非构造性证明主要通过反证法实现。lambda演算中所有东西都是函数,例如自然数就可以表达成一个可分辨的零函数(通常用恒等函数表示),和一个后继函数(用于得到下一个自然数),其构造性特征成为数理推论的基础。

如康德所说,“数学知识是从概念的构造得出来的理性知识。构造一个概念,意即先天的提供出来与概念相对应的直观”,后来,19世纪德国的克罗内克又进一步指出:“上帝创造了整数,其余都是人做的工作”。主张自然数与数学归纳法,是数学最根本的和直观上最可信的出发点,其它一切数学对象都必须能在有限步骤内,从自然数中构造出来,否则就不能作为数学对象。

lambda演算完美的结合了作为表达式的动态运算特征,以及作为数据的静态存在特征,其描述形式(指匿名申明与即时定义)非常适合推导事物的本原,下面我们换一个角度诠释lambda的存在意义。

按照我们的生活经验,认识一个物体首先要从静态角度观察,高速运动中的物体是很难识别的。编程语言中的数值、变量,以及已定义的函数,都是这一类静态物体。另外,静态物体若不与其它物体交互作用,就不能展现其功能,它的存在仅仅是概念,编程语言中的表达式,或者说函数调用,实际就是这种交互作用的形式化描述。lambda将两者合二为一,如上面Python例子,“lambda x: x * iValue” 定义是上下文相关的(否则会找不到iValue), 在参数传递期间,lambda函数是静态存在,以数据形成传递,而同时它又是匿名的,该函数并不对他人产生影响。当这种没有副作用的演算方法,用于获得“针对任一输入得到确定输出”的计算方法时,我们说它具备自我描述能力。

自描述在众多FP语言中很容易找到例证,比如有人曾用LISP语言自身,开发出LISP解释器,下面我们用Python脚本再举一个浅显例子:

    def PrintEval(s,Global,Local):
      if s == 'exit': return 1
      else: 
        try: print eval(s,Global,Local)
        except: from traceback import print_exc; print_exc()

    def test():
      iValue = 99
         
      dbLoop = lambda d1,d2:PrintEval(raw_input('DB>'),d1,d2) or dbLoop (d1,d2)
      dbLoop(globals(),locals())

本例定义一个标识为dbLoop的lambda函数,lambda函数中使用or连接它自身,由此构造出循环逻辑。调用dbLoop后系统进入交互调试状态,循环执行用户键入的命令直至输入exit退出。这个例子尽管简单,我们显然做到交互执行器自我描述了。

本文中自我描述是指对于所有表达式E,在新构造的解释器I下计算E得到的结果,与直接计算E得到的结果值相同,这不是说拿构造物替代原有功能。

受限于Python的语言能力,上述自描述代码没有用纯粹的FP方式实现,其中PrintEval函数定义及dbLoop变量是命令式的。命令式风格让自描述过程无法自包含,会遗留一些影响(如上例PrintEval与dbLoop定义),比如交互调试中,用户可能修改dbLoop的属性,而dbLoop要提供正常的调试功能,须保证它在使用过程不受影响。

按照自描述的定义,所有表达式E在自描述前后执行是等同的,所以,从严格意义上讲,上述代码还没实现真正的自描述,产生该问题的根本原因是引入了命令式风格。我们把代码改造成如下形式,可以消除dbLoop变量的影响。

    def LoopEval(Global,Local):
      while 1:
        s = raw_input('DB>')
        if s == 'exit': return
         
        try: print eval(s,Global,Local)
        except: from traceback import print_exc; print_exc()
        yield s

    def test():
      iValue = 99

      apply( lambda d1,d2: [item for item in LoopEval(d1,d2)],[globals(),locals()] ) 

从理论上说,某系统如果是自描述的,它需要支持完整的lambda演算,以及,该系统中至少能找出一个不动点。大家不妨进一步练习,看看上面例子中,LoopEval函数定义是否也可消除(提示一下,请大家分析print与yield两个命令式语句,看看能不能找到可替换函数)。

前面讲到构造性证明与非构造证明,如果觉得不好理解,建议大家结合上面例子再细琢磨。命令式编程与函数式编程反映两种截然不同的世界观,命令式从已然经验推导行为规则,而函数式从非确定经验推导与源头可能等价的规则;前者反映了一种静态的、先验性的认知,而后者是动态的、后天学习的、尚在成长中的认知。

命令式语言好比是无所不知、无所不能的上帝,凭他自己的能力与意愿,制造出精美绝伦的亚当与夏娃。而函数式语言则是人类自身,任何个体都不是全能的,但通过自我演进直至最终趋于完美。

还有一个比较恰当的隐喻,拿人工生命科学中常用的两个术语概括:命令式风格是生命如我所识(life-as-we-know-it),而函数式风格是生命如其所能(lift-as-it-could-be),前者拿一种经验套用另一种经验,是仿他意识,而后者表现出一种自我意识。再通俗一点来讲,针对同一功能实现,命令式语言会说:“这事我看如何如何...”,而函数式语言会说:“这事本来如何如何...”。


不能两次踏进同一河流,还是一次也不能

早期算法研究表明,基于自动机、符号操作、递归的函数定义和组合学,都具等备等价的描述能力,尽管命令式与函数式的语言风格存在很大差异,但他们的计算能力是同等的,也即符合著名的丘奇论题:任何符合直观理解的计算模型都具有相同的计算能力。随着时间推移,人们已经形式化的证明了各个算法之间具有等价性,其中包括图灵自动机与lambda演算。

图灵机是一种由一个无限长纸带、一个读写头组成的机器,它具备访问纸带上任一单元的能力,由内部程序驱动不断的修改存储带上各单元的值,最终完成各项运算。图灵机是一种非常简洁的计算模型,具备完整的计算能力,我们只要改变它的内部程序,完全可以胜任现代计算机能做的工作。

大家知道,计算程序如果有Bug可能会造成死机,图灵机具有等价能力,它如果运行特定控制逻辑,会不会也出现类似情况呢?下面我们分析Alan Turing提出的图灵停机问题:

存不存在一个图灵程序,比如说P能够判断出任一程序X是否会在输入Y的情况下陷入死循环?若有死循环P将返回TRUE,否则返回FALSE。
我们尝试一劳永逸的构造出这种检查死循环的程序P,该程序同样可作用于自身,即,将P自身作为输入,运行P最后上报检查结果。如果假定P在某种输入情况下不存在死循环,那它最终上报结果是FALSE,但此时如果P存在死循环,那它应该上报TRUE,现在问题出来了,此条件下P程序有死循环,那它能否上报结果呢?

图灵停机是个悖论,它尝试在P程序尚不存在,或定义尚未完成时,就要求自身能被分析,这好比一条巨凶猛的蟒蛇,它有能力吞吃世界上任一条蛇,现在我们发出指令让它吞吃自己,于是蛇头咬蛇尾,有一半下肚了,另一半还能不能继续吞咽下去?!

图灵停机反映出一个深刻问题:自我演进的系统能否始终处于自我否定状态?GCC用来编译C代码,而它自身也是用C语言开发的,我们拿GCC编译自身源码,一次编译造就GCC一次自我演进,把图灵停机问题套用到GCC,就是GCC停机问题:

存不存在一个现成GCC版本,它总能顺利编译因自身语法调整而修改的源码?

当然,答案是否定的,我们先讨论一个更为广泛的命题——不动点定理,之后回头分析GCC停机的原因。

一维布劳威尔不动点定理:设f(x)是定义在[0,1]上的连续函数,且满足0≤f(x)≤1,则必存在x0∈[0,1],使f(x0) = x0。

由于f(x)是连续函数,当x在0到1的区间连续变化时,f(x)曲线必然跨越下图A区与B区,或者与分隔线(y = x)的端点重合,交叉点(或重合的端点)就是不动点x0,重合点既满足y = f(x),也满足y = x,即f(x0) = x0。


上面f(x)是指定区间内的连续函数,如果应用到FP编程,函数可以成为f(x)的传入参数,也可以是它的返回值,在自我描述的系统中,Y是这样的运算符,它作用于任何一个函数 F,就会返回一个函数X,再把F作用于这个函数X,还是得到X。即“(Y F) = (F (Y F))”,由于Y作用于F返回的X,在其操作前与操作后维持稳定,我们称它为F的不动点。

X代表了F中的某种稳定的本质性的东西,而Y操作的目的是要发现这种本质特征。从上图我们还可以看出,一个系统的不动点可能有多个,前面提到的编程语言等价性,不同计算模型等价性,都侧面验证了这一情况。

即使是同一编程语言,完成一次自我演化都可以凭借不同的不动点,比如,if、else语句可以用带短路功能的与或逻辑代替,循环语句能用递归调用构造,如前面举例的Python代码,用OR连接自身后调用就构造出完整的循环处理。条件选择与循环处理还可以用更低层次的汇编指令去实现,一个条件跳转指令就够了。

我们回到GCC停机问题,这个命题是说GCC“总能”编译自身变化中的代码,原理上说它的变化是指定区间内的连续函数,如果GCC基础语法调整了(比方删除if、else,改用其它表达方式),必然可能破坏已有不动点。尽管一次演化可选择不同的不动点,但这是普遍意义上讲的,针对GCC特例,本命题的前提是:当前编译器与被编译代码都还是GCC,并不变成其它语言(如汇编语言),不动点不存在了,就无法支持自我重构。什么叫GCC还是GCC?请大家体会一下,这句话是不是很有嚼头。

在一次编程中,我们把无副作用的lambda演算归结为函数式风格,把有静态规格定义的变量或命令语句,归结为命令式风格。推而广之,局部演化特征必然反映到整体,自描述系统的一次演进过程中,存在不动点可认为是命令式风格,而规格平滑演进属函数式风格。

这种动静结合、交互轮替的规则,遍布于有机体各个角落,及其发展进程的每一时刻。映证了辩证法观点,运动是绝对的,静止是相对的,而且是必须的,否则导致不可知论。比如GCC演进,按照赫拉克利特的说法,“人不能两次踏进同一条河流”,GCC演化中仍具有暂态的稳态性状,所以,GCC还可以看成GCC,而按赫拉克利特的学生克拉蒂鲁的说法,人连一次也不能踏进同一条河流,程序经过修改就不再是GCC,GCC停机无从谈起了。

无论图灵停机,还是GCC停机,提出问题者是以静态角度,命令方式提出质疑的,如果换成函数式思维去观察,这两个命题是不是还存在?

假定我们自身就是万能的图灵机,如果真要检查自身逻辑处理是否有死循环,现实一点,我们会再造一台图灵机,让那一台图灵机做检查,因为当自身还不能运转时,不可能检查自身的。对于迭代中的GCC,假定我们自身就是GCC程序,哪部分改了就是修改了,如凉水浇背,冷暖自知,并不是我们非得要给个概念,前一刻叫啥,后一刻又叫啥。

下面我们轻松一下,欣赏一段禅宗公案,《大慧普觉禅师语录.卷二一》中记录:

僧问赵州:“狗子还有佛性也无?”州云:“无。”看时不用博量,不用注解,不用要得分晓,不用向开口处承当,不用向举起处作道理,不用堕在空寂处,不用将心等悟,不用向宗师说处领略,不用掉在无事甲里。但行住坐卧时时提撕“狗子还有佛性也无?”,“无。”提撕得熟,口议心思不及,方寸里七上八下,如咬生铁橛,没滋味时,切莫退志。

(注:提撕是指佛教徒参究古公案)

注意,这里“无”并非“有”的对立面:没有,而是踏杀了“有”、“没有”、“有且没有”、“有或没有”、“非有且没有”、“非有或没有”...,直到言亡虑绝才悟得的道理。


总结

命令式与函数式是编程语言的两大流派,它们拥有各自的优势与劣势。总体上说,命令式语言更加符合人们的思维习惯,人总是习惯以自我为中心观察世界的,以静态的、有差别的(一个个对象)方式认识周边事物。而函数式语言,在某种意义上说,有点违反人性,它无时不刻都以函数为中心,用括号串接各个表达式(称作剑桥波兰记法),其风格让大多数编程人员范晕。所以,单凭这一点,命令式语言占据现实应用的绝对地位并不奇怪。

事实上,真正纯净的FP语言并不多,函数式语言也往往增加命令式特征,象LISP,尽管被公认为函数式编程的鼻祖,但它还不是纯正的函数式编程,在LISP中可以用let指令赋值,其它的,如Scheme、ML等都不是纯正FP语言。

函数式语言除不够友好,运算效率低下外,他的优点非常鲜明,动态定义、无副作用、良好伸缩性、强大的演进能力等,都是命令式语言难以企及的。由于命令式与函数式各有优势,两者走向融合是近些年来编程技术的发展趋势,尤其随着Python、Ruby等动态语言逐步深入人心,FP编程渐渐流行开来。目前像Java、C#等新兴语言,已经融入了不少FP编程要素,借助反射等机制,命令式语言也拥有很强的动态编程特性。

 

参考资料:
  1. 《Programming Language Pragmatics》,美国,Michael L.Scott著,《程序设计语言----实践之路》,裘宗燕译,电子工业出版社
  2. 《皇帝新脑》牛津大学,罗杰.彭罗斯著,湖南科技技术出版社,许明贤、吴忠超译
  3. 《数字创世纪:人工生命的新科学》,李建会、张江编著,科学出版社
  4. Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I,John McCarthy, Massachusetts Institute of Technology,April 1960,
  5. Why Functional Programming Matters,John Hughes paper, dates from 1984
  6. Charming Python: Functional programming in Python, David Mertz, 01 Mar 2001

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值