1.5.1 要设计怎样的语法 编程语言有很多种,C、C++、Java、C# 等都是 面向过程的编程语言( C++、Java、C# 虽然也被称为面向对象,但可以把面向对象看作是面向过程的一个派生)。目前看来,虽然面向过程的语言是主流,但还存在Haskell、ML 这样的 函数式编程语言 。函数式编程语言就是“变量值无法被更改”的一种语言*。 对于已经习惯了面向过程语言的人来说,肯定会想“变量值无法更改还怎么写程序呀”。其实这类语言已经编写出了很多实用的程序。在函数式编程的基础上发展出了如Prolog 这样的 逻辑编程语言 以及被称为 并行程序设计语言 的Erlang。 不过目前被广泛使用的仍然是面向过程的编程语言,本书中的代码示例使用的也都是面向过程的语言风格,当然里面还会加入面向对象的一些功能实现。在本书中,除了会有C++、Java、C# 这种基于类的面向对象之外,也会涵盖类似JavaScript 这种没有类的面向对象。 语法层面上,会使用类似C 语言的风格。crowbar 的示例代码如代码清单1-1所示,Diksam 的示例代码如代码清单1-2 所示。 代码清单1-1 crowbar 版FizzBuzz 代码清单1-2 Diksam 版FizzBuzz *从这个定义来说,Lisp严格讲还不能算是函数式编程语言。 顺便说一下这个名为FizzBuzz 的小程序,其运行机制如下: 输出从1到100的数字,如果为3的倍数时,则将数字替换为Fizz,5的倍数时则输出Buzz,同时为3与5的倍数时输出FizzBuzz。 这个小程序引自下面的文章。文章大意是建议企业在面试程序员时,至少应聘者能写出这种程度的代码再考虑录用。 ◎为什么自称程序员的人写不出程序?http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm 看了示例就能明白,无论crowbar 还是Diksam,都是与C 语言非常类似的语言。 如上所述,本书虽然会创造一门新语言但仍然会用到C 语言,所以本书所面向的读者应该是已经掌握了C 语言的(还没有掌握的人可以先去学习一下)。因此如果选择C 语言风格的语法,读者应该会感到很亲切,更重要的是笔者本人已经习惯了Java、C# 这种以C 语言为基础的编程语言。 C 语言是很老的语言了,这门语言不是在前期经过严谨的设计,而是在项目中一边实践一边慢慢发展起来的,因此语法上难免有很多考虑不周的地方。比如在C 语言中赋值使用 =,即数学中的等号。而C 程序员在初学者阶段编写 if 语句时,肯定免不了会写成这样: if (a = 0) { ← 应该写"==" 但是写成了"="… } 这样惨痛的教训至少也要经历一次吧。赋值在Pascal 等语言中,一般使用 :=。如果让一个没有编程经验的人来学习,Pascal 这种语法应该更加友好一些。 不过我现在是要制作一门新的编程语言,而使用这门新语言的人应该都已经习惯了C 语言的运算符,如果这里将赋值运算符定为 := 的话反而会引起混乱,说不定我自己就先头晕了。所以经验之谈是,语法上的些许优劣还是要给“习惯”让步的。 ——出于这种考虑,我最终决定制作一门与C 语言类似的编程语言。 决定语法风格是编程语言创造者的特权。如果顾虑用户习惯,可以参考并整合已有的编程语言。当然,也可以完全不考虑用户的感受,去创造一门“理想的语言”。虽然我是以C 语言的语法为基础,但还是想到了以下几点可以改进的地方。 1. if条件在C语言中,如果按条件执行的语句只有一句,则 {}可以省略。但是这经常会造成混乱,很多项目的编码规范中都会规定必须包含 {}。因此最好在语法层面直接将 {}设置为不可省略(crowbar、Diksam均如此)。 2. 既然已经将 if 条件中的 {}设置为不可省略,那么 if后面的 ()要怎么办呢?(关于这一点,我起初在crowbar中尝试了一下省略 if的括号,结果发现在crowbar中 ()是不可省略的。) 3. 伴随着语言的逐步完善,考虑到要增加一些关键字(参考2.3.1节的补充知识),此时再处理与已存在程序的变量名相冲突的问题就比较麻烦,所以考虑在所有的变量前加上 $(Perl或PHP等的解决方式),或者将关键字全部以大写字母开头(Modula-2等的解决方式)。 4. switch case 语句中,最好能去掉忘了写 break就会进入下一个 case这种容易产生问题的设计(Java没有改进这一点,C#则做了一些半吊子的改进)。 5. switch case语句中,如果没有进入任何一个 case条件分支,也没有写 default分支,那么在运行时直接报错会不会更好一些(Pascal就是这样处理的)? 6. 编码规范通过缩进来约束怎么样?比如像Python那样通过缩进来表明逻辑结构。 7. 对于我来说,阅读Python风格的代码还有些吃力,因此是不是做成像C语言那样用花括号包裹语法块、把强制缩进的检查交给编译器去做比较好呢? 我希望读者朋友们也能够用好语言开发者的特权,不断去追求“更加理想的语言”。呃,虽然我这样讲可能会被说成是站着说话不腰疼吧。 1.5.2 要设计怎样的运行方式 程序员中应该无人不知,编程语言有编译型语言和解释型语言两种。 编译型语言中,C 和C++ 比较有代表性。这类语言通常会将程序员编写的程序源代码,最终输出为机器码的可执行文件。 但是想要输出机器码的话,必须首先掌握机器码才行。即便学习了机器码并写出了编译器,该编译器也无法输出供其他型号CPU 运行的文件*。 这类生成机器码的编程语言的优点是运行速度非常快,但是编译器性能优化的相关技术,学习起来非常有难度。另外,在自制编程语言的理由中曾经列举了“可以用编程语言扩展应用程序”这一点,而输出机器码的编译器并不适合这个用途。因此本书中会选择解释型语言。 虽说“解释型语言”只是一个词,但是其实现方法又分很多种。 解释型语言的“解释”一词源自英语的 interpreter ,是“能进行翻译的物体”的意思。编译器将源代码翻译为机器码,之后CPU 直接运行机器码就可以了。与此相对的解释型语言,则将程序员编写的源代码通过解释器这一程序一边解析一边运行——这种公式化的定义看起来只有简单的两个步骤,但现实中几乎不存在这么单纯的解释型语言(DOS 的批处理脚本或UNIX 的SHELL 脚本是最接近解释型语言的定义的)。虽说名为“解释型语言”,但其中的大多数都会将源代码临时转换为某种中间形态。比如有代码清单1-3 这样的代码。 代码清单1-3 简单的if语句 *为了解决这个问题,一般的编译器都会将依赖CPU 生成的机器码的部分单独归为一个名为Backend 的模块,根据不同的CPU 可以更换相应的Backend,就可以支持其他型号的CPU 了。 从机器的角度看,源代码其实只是一些文字的排列组合而已,机器是无法直接运行的。现在大多数编程语言,都会将代码转换成一种叫分析树(parse.tree,也叫语法分析树或语法树)的东西。上面的代码如果做成分析树,则如图1-1 所示。 图1-1 分析树示例 Perl*、Ruby 等语言,一旦将代码转换为分析树后,分析树将无法再还原回源代码。 本书第2 章以后所用到的语言crowbar 就是采用这种运行方式的语言。 对于这类语言来说,从源代码到分析树的构建过程还是得称为“编译”。但是这里的编译器是在程序启动时自动执行的。由于分析树会生成在内存里,因此不会生成目标代码或目标文件,所以程序员(用户)一般意识不到有编译器在执行。这类语言如果存在语法错误,会在刚开始运行时就被报出来,这正是源代码被一次性全部读入并构建分析树的证明。如果是纯粹的解释型语言,如批处理脚本或SHELL 脚本,则会运行到有语法错误的地方才会报错。 那么,相对于Perl、Ruby 这样的运行分析树型语言,在Java 等语言中,取代分析树的则是更底层的字节码,然后通过解释器运行字节码。字节码只是一些简单的数字排列,为了尽可能地让人读懂字节码,字节码中的所有指令都被加上了一些名为助记符(mnemonic)的字符,代码清单1-3 的源代码经过这样一番处理之后最终会变成代码清单1-4 的样子(源代码中的 printf 改为 System.out.println,并使用javap 输出)。 代码清单1-4 Java 的字节码 本书第5 章以后所用到的语言Diksam,就是采用这种运行方式的语言。 在Java 中,编译器生成的字节码会被保存在class 文件中。但是在Diksam中,编译器会在程序启动时执行,因此字节码保存于内存中,不会生成类似class文件的东西。由此可以看出,从用户的角度出发,不需要意识到Diksam 内部其实有字节码在执行。Python 也是使用了类似的处理机制。 *Perl 6 还不知道什么时候出来,就不管它了:-) 补充知识 “用户”指的是谁? 前文曾写道“因此程序员(用户)一般意识不到有编译器在执行。”通常来说,用户是指使用程序员编写的程序的人,但是在这里,因为我们是要制作一门编程语言,所以本书中的用户应该是指使用我们制作的编程语言的人,即程序员。 这种指代在操作系统、类库、编程语言等面向程序员的文档中经常出现,不过可能有读者会有误解,在此特别补充说明一下。 补充知识 解释器并不会进行翻译 在很多入门书中,提到编译器与解释器时,一般会采用以下说明: 编译器会将源代码一次性全部翻译为机器码。 与此相对的解释器,不会事先做一次性翻译,而是在运行的同时,逐行分块地将源代码翻译为机器码。 请允许我说句老实话,这样的说明是完全错误的。 解释器会将源码或分析树解析为字节码这种中间形态,并且一边解析一边运行,但是解释器并不会将源码翻译为机器码。 Java 或.NET Framework 都具备在运行的同时将字节码转换为机器码的功能,这叫作“JIT(Just-In-Time)编译”技术,而这部分技术并不属于解释器。 那么解释器具体是如何运行程序的呢?读到后面你就会明白了。