动静之间,不变的本质
—— 我的程序语言实践
引子
源于SD2.0大会的召开,以及拙作《JavaScript语言精髓与编程实践》一书的出版在即,CSDN、博文视点以及《程序员》的编辑都希望我写一点关于语言的东西。我已经为这个问题苦恼了很久,因为我不知道可以说什么,既可以与先行者有别,又可以使后来者为鉴。
这下便借用《程序设计语言实践》一书的书名,讲讲我的经历吧。
从动态语言到静态语言
我所学的第一门语言其实是数据库编程语言DBASE,参加的是学校的一个暑期的关于微机操作的培训——程序设计语言只是其中很小的一部分,我们得从键盘键位之类的开始学起。就这样,我也只学了十天。这是在1994年,当时用的是DBASE III。DBASE是解释执行的、弱类型的一种语言,我用它写过数据库的前置的口令检测程序。很快我就换成了BASIC,换这门语言的原因是当时要参加省赛区的一个竞赛,我用了一个月略多的时间来学习它就参赛了,结果是第二名。在后来的学习中,我用BASIC写过内存处理、端口处理程序。到了1997年的时候,我的一个朋友的毕业设计要用BASIC来写,当时我已经丢下BASIC两年了,但还是用了一晚的时间完成了一个考试管理系统,包括班级、学科管理、成绩管理和档案管理(含磁盘检测以上报档案)。当时我用了一个名为Turbo Basic的东东,是Borland做的最后一款BASIC语言的工具,据内幕说是因为与微软存在协议,后来Borland再也没出过这一语言的工具。
(*) 顺便补充一点,那个微机培训的课程是一个月,而我当时对文学感兴趣,因为要去北京参加一个笔会而只学了十天。我对文学的兴趣持续到1999年,在2000年的时候,一篇早期的文学作品拿了网易首届网络文学大届的散文金奖(这个大赛也只办了一届,哈哈)。而对文字的喜好则持续到现在,成了我写书作文的根基与动力。
在DOS时代还有一种可编程的批处理(当然现在在Windows系统中也有,只是用的人实在不多了),我曾经用它与BASIC和汇编结合起来,写过系统加锁程序。这些大概都是在1996年之前的事了。
我学习编译型语言的时间晚于解释型的,最初是在1995年初开始自学《数据结构》一书,那一版的书就是用PASCAL语法来讲的,而我学算法语言晚于学《数据结构》,所以当时并不会PASCAL,也不可能在当时的BASIC中理解这些复杂的数据结构。因此我又买来Pascal语言入门教材自学,然而题不对板,数据结构中用的与我这本书所讲的,以及我能用的Pascal编译器三者全不靠谱,而那时候我还在死学死用的层次上,学了半年而无有小成。
然而《数据结构》却没有耽搁,对这门学科的学习成了我这么多年来理解编程的基础。再后来,我又开始自学汇编语言、算法语言(Pascal)和操作系统原理,到了1996年初,我基本上已经完成了对语言和系统的自学。这个时候,我的语言选择已经从Basic转到了Pascal。
换言之,我从动态语言走到了静态语言。不过这样说,我可能被立即反问:它们是动态语言吗?我慎之又慎的思考之后的回答,仍然“是”。前面提到的DBASE、BASIC和批处理其实都有动态语言的特征——尽管这在我学习它们时是不知的。
回归动态-从Delphi到JavaScript
知道我的朋友很多是因为Delphi以及我所写的《Delphi源代码分析》一书。我从1996年开始做一些商业产品的代码,一直到2003年都是使用的Pascal/Delphi系列语言,以及一些汇编语言。我从传统过程式开发转变到面向对象开发,用了非常长的时间——超过两年。直到2003年的早些时候,我终于停下忙忙碌碌的、无止尽的代码书写,问了自己一个问题:Delphi是怎么回事?
为了给自己解答这个问题,我辞掉所有的工作,静下心来研究这门语言。准确地说,是研究静态语言的语法、语义以及面向操作系统的编译与二进制文件生成。《Delphi源代码分析》这本书讲的就样的一些内容:语言的基本要素、操作系统对语言的要求、语言的实现等等。只不过我是通过对Delphi的源代码的分析来展现这些罢了。
用了一年的时间,我终于看明白语言在“结构”方面的真相:所有的语言效果、语法以及二进制的执行能力,原来不过是操作系统理解的一堆“可执行和可存取的数据结构”。“编译”这一过程,无非是把这些细节隐藏起来,让程序员以为自己在写一种非常高级的、逻辑的、有趣的代码,而忽视掉背后的本质部分:静态的数据与指令。
如同我此前对于Pascal/Delphi的盲目一样,我学用JavaScript之初也是盲目的。我选择JavaScript的唯一原因,只是因为我在Web上开发时找不到第二种可以通用的语言(如果当时我能选择一种类pascal语法的语言,我一定会错失学习JavaScript的良机)。
我从1998年开始使用JavaScript,这离这门语言被创生出来不过三年时间——以语言的历史而言,这算得上是“追新”了。在最初我无非是把它当成一种脚本化的过程式语言,以及用来响应网页里的OnXXXX事件的一些代码。但在一两年之后,在我深受Delphi中的面向对象编程思想的影响之后,我发现JavaScript令我不堪忍受:它毕竟不是一门具有完整的对象特性的语言。于是我开始试图实现JSOOP:JavaScript的面向对象编程。
这一设想以及一些实践开始于2002年,但直到2004年初我才真正着手实施这个计划,到了2005年末这个项目延伸为现在的Qomo(Qomolangma OpenProject)。而我另一方面的计划——自2005年初开始写的一本名为《B端开发》的书,也终于因为Qomo项目而被放弃,变成了《JavaScript语言精髓与编程实践》。因为在Qomo开发过程中,我发现讨论JavaScript这种语言本身,远比“在浏览器端(B端)开发”更为有趣。尽管,在这其间我还用过PHP、Java与C#等等,不过相对于后面要讨论的内容来说,这些已经不重要了。
因为我对编程的理解,终于从Delphi走回JavaScript,从静态回归到动态。
JavaScript语言的基本特性
到底JavaScript是怎样的、以及为什么会吸引我呢?如今我之视见,JavaScript语言包括了四个方面的语言特性:过程式语言、面向对象语言、函数式语言和动态语言。具有过程式特性,是它入门容易的原因;面向对象特性则使它符合主流的程序设计思想;函数式是JavaScript语言的根基,而动态语言则是它的外在表现,以及强大到难于驾驭的根源。
前些时候在北京参与CSDN大会时,与一个老朋友谈到JavaScript,他说:JavaScript具有几乎所有主要语言形式的原子要素。我觉得,这个“语言原子”的概念就提得很好。的确,JavaScript在上述四个方面都表现平平,但每个方面都抓住了相应语言范型的精髓。不但如此,JavaScript还使用了一个最简而又最合理的方式来组织各种语言特性。仅以“动态”而论,《JavaScript语言精髓与编程实践》讲述了JavaScript所包括的四个方面的动态性质:
- 动态执行
- 动态类型
- 重写
- 实现动态环境的基础数据结构
源于本文篇幅,对于更深层面的问题便不讨论了,这里仅讨论一下静态执行与动态执行的某些本质上的差异。举个例子来说,下面的代码:
obj.aMethod(x,y,z);
这显然是一个对象方法调用。但对象方法是如何实现的呢?在静态语言中,因为有编译过程,所以我们把一个结构放在内存里,并使得它
- 拥有一个对象实例指针指向obj,
- 拥有一个对象方法指针指向aMethod()在代码区的地址,
- 在有效代码的前后加入处理x,y,z这些参数的代码(例如入栈与清栈)。
在执行时,我们将obj与aMethod交给执行系统,并传入指定参数(的序列),然后就可以按照既已编译的规则来执行了。
然而在JavaScript中,由于它是一个动态语言,因此编译的结果只是一个表达执行过程的语法树。这个语法树被存储为一个二叉树形式的数据结构。更细节的说,二叉树的Root/Left/Right三个节点分别表示运算符、运算元1、运算元2。以较为复杂的三元运算符“?:”来说,下面表达式:
isTrue ? expr1 : expr2
就被表达为
root(?)
/ /
left(isTrue) right(:)
/ /
left(expr1) right(expr2)
——学过数据结构的开发者一定对此不会陌生,因此我就不细讲实现和使用这个结构的过程了。
(其实三元表达式不能描述为这个结构,但这里只为了说明问题,请不要去追究它。)
不过这一结构也显然地可以陈述为一个lisp/scheme表达式。同样的道理,一个对象方法的过程亦是如此,用这样的方法来表达上面的“对象方法调用”,就可以用lisp/scheme方法来陈述:
((. obj aMethod) x y z)
正是这个细节,表明在JavaSctipt中所谓的“对象方法调用”,其本质上是函数式语言中的一个运算式。因其是一个运算式,所以能动态地(作为语法树的一部分)解释执行。
同样的方法,可以解构所有在JavaScript有关执行系统(语句、表达式、函数等)的问题,基本上都可以归结到函数式这个范围内。换而言之:函数式这个“原子”为JavaScript提供了执行和动态执行能力。
再往后,我考察了JavaScript在数据结构层面上的实现。这方面最精当(而又平凡)的解释是前两天在微软架构师有关语言的讨论中做出的:JavaScript的对象其实是“属性包”。不过,更专业而难解的词汇是“关联数组”。JavaScript使用关联数组作为动态化类型的基础,而静态的值类型仅包括布尔、数值、字符串类型(字符串兼具值与引用两方面特性)和undefined四种。当所有的类型被归结到这里时,就可以发现“动态”的另一层含义与”引用“结合了起来:
- 所谓引用,不过是动态地找到值的一种手段
另一方面,对运算系统的考察,也会被归结到这里:
- 所谓运算,本质是针对值的运算
举个实例来说,例如:
aWindow.width = obj.aMethod(x,y,z);
其中".width”运算的目的是找到一个值类型的属性;“.aMethod()”的目的是找到一个函数并调用它,然后返回某个值;“=”运算则将某值传入某个存值的属性。
所以,根源上来说,JavaScript中的所有运算都是围绕“如何得到和使用值(类型)数据”来的。对此更深入的推论是:所有的计算系统都是围绕这一根本目的来的。
在写《JavaScript语言精髓与编程实践》的过程中,我一次又一次地回顾了我对Delphi的所有理解,抛开那些将语法树静态化到内存结构和磁盘文件结构的过程,我看到:所有静态与动态语言,在本义上所追求的,无非是算法与结构的平衡——即先满足算法实现一致性,还是先满足结构实现一致性的问题。
在这十余年之中,我从动态开始,深入静态又回归动态,最终我来到了原点:程序=算法+结构。
学两种语言
在《程序设计语言实践》中对“语言”有一个分类法,将语言分类为“说明式”与“命令式”两种。Delphi以及C、C++、Java、C#等都被分为“命令式”语言范型的范畴;“函数式”语言则是“说明式”范型中的一种。我如今回顾我对语言的学习,其实十年也就学会了两门语言:一门是命令式的,一门是说明式的。当然从语言的实现方式来看,一门是静态的,一门是动态的。
这便是我程序员生涯的全部了。
我毕竟不是计算机科学的研究者,而只是其应用的实践者,因而我从一开始就缺乏对“程序”的某些科学的或学术层面上的认识是很正常的。也许有些人认为一开始程序便是如此,或者一门语言就应当是这样构成和实现的,那么可能他是从计算机科学走向应用,故而比我了解得多些。而我,大概在十年前学习编程,以及在后来很多年的实践中,仅被要求“写出代码”,而从未被要求了解“什么是语言”。所以我才会后知后觉,才会在很长的时间里迷失于那些精细的、沟壑纵横的语言表面而不自知。然而一如我现在所见到,与我曾相同地行进于那些沟壑的朋友,仍然在持续地迷惑着、盲目着,全然无觉于沟壑之外的瑰丽与宏伟。
前些天写过一篇BLOG,是推荐那篇“十年学会编程”的。那篇文章道出了我在十年编程实践之后,对程序语言的最深刻的感概。我们学习语言其实不必太多,深入一两种就可以了。如果在一种类型的语言上翻来覆去,例如学C、Delphi、Java、C#……无非是求生存、讨生活,或者用以装点个人简历,于编程能力上提高是不大的。更多的人,因为面临太多的语言选择而浅尝辙止,多年之后仍远离程序根本,成为书写代码的机器,把书写代码的行数、程序个数或编程年限作为简历中最显要的部分。这在明眼人看来,无过是熟练的砖头工而已。
《大道至简》中说“如今我已经不再专注于语言”。其实在说完这句话之后,我就已经开始了对JavaScript的深入研究。在如此深入地研究一种语言,进而与另一种全然有别的语言比较补充之后,我对“程序=算法+结构”有了更深刻的理解与认识——尽管这句名言从来未因我的认识而变化过,从来未因说明与命令的编程方式而变化过,也从来未因动态与静态的实现方法而变化过。
动静之间,不变的是本质。我之所以写这篇文字,并非想说明这种本质是什么亦或如何得到,只是期望读者能在匆忙的行走中,时而停下了脚步,远远地观望一下目标罢了。
而我,此时刻,正在做一个驻足观望的路人甲。