自制编程语言
上QQ阅读APP看书,第一时间看更新

1.5 要制作怎样的语言

1.5.1 要设计怎样的语法

编程语言有很多种,C、C++、Java、C#等都是面向过程的编程语言(C++、Java、C#虽然也被称为面向对象,但可以把面向对象看作是面向过程的一个派生)。目前看来,虽然面向过程的语言是主流,但还存在Haskell、ML这样的函数式编程语言。函数式编程语言就是“变量值无法被更改”的一种语言从这个定义来说,Lisp严格讲还不能算是函数式编程语言。

对于已经习惯了面向过程语言的人来说,肯定会想“变量值无法更改还怎么写程序呀”。其实这类语言已经编写出了很多实用的程序。在函数式编程的基础上发展出了如Prolog这样的逻辑编程语言以及被称为并行程序设计语言的Erlang。

不过目前被广泛使用的仍然是面向过程的编程语言,本书中的代码示例使用的也都是面向过程的语言风格,当然里面还会加入面向对象的一些功能实现。在本书中,除了会有C++、Java、C#这种基于类的面向对象之外,也会涵盖类似JavaScript这种没有类的面向对象。

语法层面上,会使用类似C语言的风格。crowbar的示例代码如代码清单1-1所示,Diksam的示例代码如代码清单1-2所示。

代码清单1-1 crowbar版FizzBuzz

      for(i = 1; i < = 100; i++){
            if(i % 15 == 0){
            print("FizzBuzz\n");
            } elsif(i % 3 == 0){
            print("Fizz\n");
            } elsif(i % 5 == 0){
            print("Buzz\n");
            } else {
            print("" + i + "\n");
            }
      }

代码清单1-2 Diksam版FizzBuzz

      int i;

      for(i = 1; i < = 100; i++){
            if(i % 15 == 0){
                println("FizzBuzz");
            } elsif(i % 3 == 0){
                println("Fizz");
            } elsif(i % 5 == 0){
                println("Buzz");
            } else {
                println("" + i);
            }
        }

顺便说一下这个名为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语句时,肯定免不了会写成这样:

这样惨痛的教训至少也要经历一次吧。赋值在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运行的文件为了解决这个问题,一般的编译器都会将依赖CPU生成的机器码的部分单独归为一个名为Backend的模块,根据不同的CPU可以更换相应的Backend,就可以支持其他型号的CPU了。

这类生成机器码的编程语言的优点是运行速度非常快,但是编译器性能优化的相关技术,学习起来非常有难度。另外,在自制编程语言的理由中曾经列举了“可以用编程语言扩展应用程序”这一点,而输出机器码的编译器并不适合这个用途。因此本书中会选择解释型语言。

虽说“解释型语言”只是一个词,但是其实现方法又分很多种。

解释型语言的“解释”一词源自英语的interpreter,是“能进行翻译的物体”的意思。编译器将源代码翻译为机器码,之后CPU直接运行机器码就可以了。与此相对的解释型语言,则将程序员编写的源代码通过解释器这一程序一边解析一边运行——这种公式化的定义看起来只有简单的两个步骤,但现实中几乎不存在这么单纯的解释型语言(DOS的批处理脚本或UNIX的SHELL脚本是最接近解释型语言的定义的)。虽说名为“解释型语言”,但其中的大多数都会将源代码临时转换为某种中间形态。

比如有代码清单1-3这样的代码。

代码清单1-3 简单的if语句代码中的hoge、 piyo这两个单词,经常在输出无意义的语句时使用(多见于日本,英语国家则较多使用foo、 bar)。详细请参考以下的页面:http://avnpc.com/pages/devlang#hoge

      if(a == 10){
          printf("hoge\n");
      } else {
          printf("piyo\n");
      }

从机器的角度看,源代码其实只是一些文字的排列组合而已,机器是无法直接运行的。现在大多数编程语言,都会将代码转换成一种叫分析树(parse tree,也叫语法分析树或语法树)的东西。上面的代码如果做成分析树,则如图1-1所示。

图1-1 分析树示例

PerlPerl 6还不知道什么时候出来,就不管它了:-)、Ruby等语言,一旦将代码转换为分析树后,分析树将无法再还原回源代码。

本书第2章以后所用到的语言crowbar就是采用这种运行方式的语言。

对于这类语言来说,从源代码到分析树的构建过程还是得称为“编译”。但是这里的编译器是在程序启动时自动执行的。由于分析树会生成在内存里,因此不会生成目标代码或目标文件,所以程序员(用户)一般意识不到有编译器在执行。这类语言如果存在语法错误,会在刚开始运行时就被报出来,这正是源代码被一次性全部读入并构建分析树的证明。如果是纯粹的解释型语言,如批处理脚本或SHELL脚本,则会运行到有语法错误的地方才会报错。

那么,相对于Perl、Ruby这样的运行分析树型语言,在Java等语言中,取代分析树的则是更底层的字节码,然后通过解释器运行字节码。字节码只是一些简单的数字排列,为了尽可能地让人读懂字节码,字节码中的所有指令都被加上了一些名为助记符(mnemonic)的字符,代码清单1-3的源代码经过这样一番处理之后最终会变成代码清单1-4的样子(源代码中的printf改为System. out.println,并使用javap输出)。

代码清单1-4 Java的字节码

      0: bipush 10
      2: istore_1
      3: iload_1
      4: bipush  10
      6: if_icmpne        20
      9: getstatic
    12: ldc
    14: invokevirtual
    17: goto     28
    20: getstatic
    23: ldc
    25: invokevirtual

本书第5章以后所用到的语言Diksam,就是采用这种运行方式的语言。

在Java中,编译器生成的字节码会被保存在class文件中。但是在Diksam中,编译器会在程序启动时执行,因此字节码保存于内存中,不会生成类似class文件的东西。由此可以看出,从用户的角度出发,不需要意识到Diksam内部其实有字节码在执行。Python也是使用了类似的处理机制。

补充知识 “用户”指的是谁?

前文曾写道“因此程序员(用户)一般意识不到有编译器在执行。”

通常来说,用户是指使用程序员编写的程序的人,但是在这里,因为我们是要制作一门编程语言,所以本书中的用户应该是指使用我们制作的编程语言的人,即程序员。

这种指代在操作系统、类库、编程语言等面向程序员的文档中经常出现,不过可能有读者会有误解,在此特别补充说明一下。

补充知识 解释器并不会进行翻译

在很多入门书中,提到编译器与解释器时,一般会采用以下说明:

编译器会将源代码一次性全部翻译为机器码。

与此相对的解释器,不会事先做一次性翻译,而是在运行的同时,逐行分块地将源代码翻译为机器码。

请允许我说句老实话,这样的说明是完全错误的。

解释器会将源码或分析树解析为字节码这种中间形态,并且一边解析一边运行,但是解释器并不会将源码翻译为机器码。

Java或.NET Framework都具备在运行的同时将字节码转换为机器码的功能,这叫作“JIT(Just-In-Time)编译”技术,而这部分技术并不属于解释器。

那么解释器具体是如何运行程序的呢?读到后面你就会明白了。