语言理论的概念和误解
本着“理论指导实践”,既然要做,不可不先做好工作。。。装逼结束,其实这篇东西跟要写一个语言直接关系不大,主要来由是在群里或和其他人的一些讨论。有人说,引起程序员战争的最好办法就是宣称某某语言好或者不好,这似乎是真的,前不久下班时起了个头,结果某些人居然吵到半夜
平心而论,每个语言都有优缺点,这当然是废话,据观察,争论很多时候源于两点:第一,个人喜好不同,有的人有偏爱,你觉得说了一句中肯的话,在他看来就是诋毁,不过这个是人和人的问题,姑且忽略,第二点是我觉得比较重要的,就是语言理论本身的一些模糊性,使得双方心里明白是怎么回事,结果说出来就是另一回事
“我花了几个星期…试着弄清楚“强类型”、“静态类型”、“安全”等术语,但我发现这异常的困难…这些术语的用法不尽相同,所以也就近乎无用。”说这话的是Benjamin C. Pierce,编程语言专家,《Types and Programming Languages》和《Advanced Topics in Types and Programming Languages》的作者
语言理论的很多概念都比较模糊的,这并不是说它们没有一个精确的定义,实际上只要是概念,就是人提出来的,既然是人提出来的,自然可以把它做的很精确,总的来说,我觉得有几个原因
首先是历史和技术发展的原因,在早期的时候,面对的问题是比较简单的,或者说比较清晰更准确,因为用机器语言写汇编器,再用汇编器写编译器,再对编译器做各种工作实在说不上“简单”,但总的来说没有其他一些乱七八糟的事情,像fortran,虽然它的编译器开发了18个人年,但它最早的版本功能在现在看来是很简单的,不需要考虑复杂类型(只有整数,浮点,数组,连结构体都没),不用考虑GC,不用考虑面向对象(还没出来),还不用过度考虑各种吐槽,反正大家都在纸带上打孔,能弄出个高级语言就已经石破天惊了。从这个角度考虑,其实C语言非常简单,就那么几个关键字,规则也很明确,只是难用而已,就好比围棋比象棋规则简单很多,但要下好就难很多了。而现在常用的很多语言,说起来有几十年历史,但也都是上世纪90年代的事情了,而且发展速度越来越快,C语言已经很久很久没有变过了,而某些语言每年都能更新个版本,换一些语法,在实现上,各种技术都有融合,这样一来,有些概念难免就跟不上时代,而有些概念因为过于纯粹,已经不适合修饰已有的事物了
然后是广义和狭义的区别,概念定义这个事情,有个矛盾的地方,如果定义太广义了,覆盖面太宽,如果太狭义,又不得不引入更多的概念,研究理论的人容易走狭义路线,钻牛角尖,讲实用的多偏向广义,我个人比较倾向于广义
第三点我觉得比较重要,是严谨的科学和自然语言表述之间的差别,比如说,我们日常可能说“在我没出生以前。。。”追究起来这其实是个病句,因为“没出生”是一个持续状态,没有“之前”一说,总不能说没出生之前已经出生过了,道理上说不通,应该说“在我出生以前。。。”,因为出生是一个事件,有一个确定的时刻,对于时刻才可以说前后,但是成天说类似这种病句,却也没有产生什么大的影响,因为就算听着别扭,大家也知道这是个啥意思
举个最简单的例子,编译这个概念,程序员大概天天都能用到,但我碰到的很多人都还觉得编译就是“把代码转成可执行文件”,早期的确是这样,现在如果不注重中间过程,很多语言也差不多是这样,不过对于java这种语言来说,.java到.class就不算编译过程了,这就有问题,因为这个过程的确是编译,至少jdk的工具是javac,c就是compile
其实编译概念在龙书一开头就讲了,(编译器)就是一个程序,它可以阅读以某一种语言编写的程序,并把它翻译成一个等价的、另一个语言编写的程序。这是一个非常广义的概念,从这个角度说,C语言的#define宏替换过程也可以看做编译过程了(带宏的C语言转换成不带宏的C语言),但一般来说,我们还是把它叫做preprocess而不是compile,大概是因为如果概念太过广义,意义就不大了吧(需要指出的是,宏替换即便简单,至少也需要词法分析过程的,还是比一般的文本处理复杂些)
编译完成后,就要执行了,这里又有很多人搞混了“解释”和“解析”两个词
我曾经不止一次听人说,python跟C不同,是解释型语言,直接解释源代码执行的脚本语言。这句话不长,但是有好几个槽点或可能误解的地方,先说“解释”这个词,其实他想表达的意思是“解析”,在这个具体的领域,解释的英文是interpreter,解析是parse,但广义上来看,这两个概念本身边界又是模糊的,解析,一般是指一个高级语言,对于其每一个执行单元,都是编译再执行,说白了就是看到一句,执行一句,这种语言在早期出现过,但是在现在已经极少了,原因非常简单,考虑如下代码:
for (i = 0; i < 100; ++ i)
{
s += i;
}
如果我们用解析执行这段代码,则要在每次循环时,对同样的代码进行编译过程(至少是词法分析和语法分析),这显然没有必要,因为代码是一成不变的,或者反过来说,解析执行的优势在于,执行过程中,代码可以实时修改,实时起效,最简单的例子,可以举linux下的cron服务,定时执行一个shell脚本,则我们可以修改这个脚本,下次就生效了,如果cron不采用解析实现,在增加任务时候预先编译,那就实现不了这种动态性
而解释则更近一步,将高级语言编译成字节码或类似的其他形式,然后也是看一条,执行一条,当然,这过程中也可以改编译后的代码(如字节码),这点跟解析一样,只不过没那么直观
好吧,懂两者的区别了么?如果说,一个是直接解释高级语言,一个是编译一步后再解释目标代码,那么请问一个高级语言和一个低级语言的分界是什么?换个方式说,如果把低级语言每条指令的判断看做编译过程,那解释器其实就是个低级语言的解析器,而高低级语言没有明显分界的话,解释和解析还有区别吗?再反过来说,如果一个高级语言符号化程度很高,编译过程非常简单,那么直接解析又跟解释有啥区别?
这就是模糊性,我原本是想说明两者的区别,结果概念出来后,发现没法严格区别(只能大致上),同样的,解释和执行也有模糊性,你可以把cpu看作是机器码的解释器,只不过是硬件实现,如果用软件和硬件来区分,那我们是不是可以说,机器码,确切点的例子,x86机器码是直接执行,java字节码是在虚拟机解释执行,所以前者是直接执行型,后者是解释型?那可以考虑这样一个例子,假设有一台计算机,它的cpu可以直接执行java的字节码(听说真有这样的机器,至少曾经设计过?),然后我们用java写一个虚拟机,来解释执行x86,那么结论是不是也要反一下了?
这其实是想说,我们日常说的一些事情,其实都是根据实际情况,有大前提的,比如我说我出门被车撞了,估计99%的人都不会想到是隔壁小孩的玩具车,这种默认保证了我们在大多数情况下能以最小的信息量来传递最大概率正确的信息。比如没有java字节码直接跑的硬件平台(或者有但是基本用不到),那么说java字节码是解释执行一般也可以接受。但是在技术上,就可能有各种误解,最大的误解就是语言和实现不分
所谓程序设计语言,说白了只是一堆语法和规则的条文而已,它只能告诉你这个东西是什么样子,但并不能给你具体的东西,日常用的都是根据这些标准实现的编译器或运行环境,所以上面那句话,python是解释型语言,本身就是错的,但也不能说python不是解释型语言,因为解释型、解析型、编译成代码执行这些东东,是实现相关的,完全可以写一个python的编译器,直接编译成x86,ppc之类cpu的机器码,同样的道理,C语言也可以有解释器实现(这个真有),不一定非要做成直接在cpu跑的
加个前提,这话就说对了,“在主流实现下”,的确每个语言只有标准,一般很少规定实现方式,不过主流实现大都是一致的,所以一般也都说python和java是解释型语言了,因为的确主要的几个版本都是解释实现。不过,太过于局限在主流实现也会有一些错误的认识,比如有人说,python占不了多核,且振振有词地说,它有GIL,对于这个,首先像上面说的,GIL并非是语言规范的事情,其次,GIL也只有标准版本有,其他不见得有,比如Jython就去掉了,至于为啥Jython可以,以后再分析。另一个例子是很多人都认为,编译型语言(这个说法严格讲当然是错的,不过我复述原话,大家知道啥意思就行了)比解释型语言执行快,碰上这种我一般都直接举java和go比对
说起java是解释型语言,以及效率这个问题,就得再深入一下了,早期java的确是解释执行,相对应地,运行就很慢,不过后面引入了jit技术,速度有了很大提升,事实上jvm的各种优化配合jit,使得很多java程序比代码上等价的C代码运行还要快,但是,jvm本身可以用C写,所以上面只说“代码上等价”,这个效率比对的问题大概这么说比较好:对于任意的java程序,存在一个等价且效率不低于它的C程序(想到离散数学了吗)。如果了解jit,或翻相关资料,可能会知道jit是运行时将字节码编译成机器码直接执行,所以说这是一个很好的混合实现的例子。这方面还有个例子是,java做高精度运算比python还慢,因为python的long是用C实现的,java的BigInteger是java自己实现,所以虽然两者大多数时候效率差距很大,具体的场景下还是有不同的结果,当然,可以用jni来实现BigInteger……
P.S.jit是运行时将字节码编译成机器码直接执行,这个从实际来说也没什么大错,但是千万别以为jit就是代码到机器码,事实上可以事先把java直接编译成机器码来执行,但没有这么做,后面再专门说这个
然后说说上面那句话最后一个词,脚本语言,这个概念也是一塌糊涂,发展到现在,脚本和高级语言的界限是越来越模糊了,最早脚本语言是为了缩短传统的“编写、编译、链接、运行”(edit-compile-link-run)过程而创建的计算机编程语言,换句话说就是源代码可以直接运行的,所以python,ruby之类的都可以划作脚本,不过按照这个定义,假如我做了一个批处理,先javac然后java,那java也成了脚本语言了,但一般认为不是,并非因为java要先编译,因为python和ruby实际上也是先编译的,这个界限非常模糊,所以还是以大多数人的认识为准吧,不用太认真
最后一个例子是功能方面的,经常碰到有人问“python能做web吗”之类的问题,对这种问题我只能说,图灵完备的语言都是等价的,只要有合适的和现实世界的接口,请问你是不是想问python做web是否方便?
所谓图灵完备,是说一个机器(概念上的)或者一门语言,能够计算一切可计算的问题,通俗说,就是和图灵机等价,也就是通用计算机,从这个角度说,我们可以很简单的判断一个语言是否图灵完备,只要看它能不能模拟图灵机就行了,而要模拟图灵机,无非就是支持循环、分支、数组、运算之类的,这基本上是个语言就支持了,反过来,由于现在的计算机的计算能力不能超越图灵机,所以两者是全等的,然后还可以推出,图灵完备的语言都是等价的
注意上面说的机器是概念上的,也就是说不是实体机,因为实体机器的内存是有限的,而图灵机的存储空间是无限的,不过一般实际中可以忽略这个差别,毕竟我们要计算的问题是有限的
语言都是等价的,所以不要纠结能不能的问题,只有合不合适,可以选择合适的语言做合适的事情,或者选择给自己喜欢的语言增加想要的特性(也就写些扩展库而已),当然,还有自己造语言的
还有一些概念,打算另外写,如果有想起来或碰到的,以后再讨论了