欢迎访问 生活随笔!

凯发k8官方网

当前位置: 凯发k8官方网 > 编程资源 > 编程问答 >内容正文

编程问答

程序语言的自我意识与仿他意识 -凯发k8官方网

发布时间:2023/12/20 编程问答 94 豆豆
凯发k8官方网 收集整理的这篇文章主要介绍了 程序语言的自我意识与仿他意识 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

注:本篇博文为重发稿件,“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 1else: try: print eval(s,global,local)except: from traceback import print_exc; print_exc()def test():ivalue = 99dbloop = 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': returntry: print eval(s,global,local)except: from traceback import print_exc; print_exc()yield sdef test():ivalue = 99apply( 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编程要素,借助反射等机制,命令式语言也拥有很强的动态编程特性。

 

参考资料:
  • 《programming language pragmatics》,美国,michael l.scott著,《程序设计语言----实践之路》,裘宗燕译,电子工业出版社
  • 《皇帝新脑》牛津大学,罗杰.彭罗斯著,湖南科技技术出版社,许明贤、吴忠超译
  • 《数字创世纪:人工生命的新科学》,李建会、张江编著,科学出版社
  • recursive functions of symbolic expressions and their computation by machine, part i,john mccarthy, massachusetts institute of technology,april 1960,
  • why functional programming matters,john hughes paper, dates from 1984
  • charming python: functional programming in python, david mertz, 01 mar 2001

  • 总结

    以上是凯发k8官方网为你收集整理的程序语言的自我意识与仿他意识的全部内容,希望文章能够帮你解决所遇到的问题。

    如果觉得凯发k8官方网网站内容还不错,欢迎将凯发k8官方网推荐给好友。

    • 上一篇:
    • 下一篇:
    网站地图