听Ruby之父畅谈编程语言的设计

本文摘自《松本行弘:编程语言的设计与实现》

1-1 自己创造编程语言的意义

通过实际创造一门新的编程语言,可以学到编程语言的设计思路和实现方法。随着开源的普及,创造新编程语言的门槛一下子降低了许多。创造编程语言不仅可以提升你作为技术者的价值,而且还可以使你从中获得很大的乐趣。

大家都知道我是编程语言 Ruby 的作者,我其实还是一个编程语言迷,对编程语言的痴迷程度无人能及。Ruby 是我出于兴趣钻研编程语言的最大成果,把它称为我兴趣的副产品可能更为贴切。副产品就能如此普及看起来很了不起,但与其把它全部归功于我的实力,倒不如说运气的成分更大。Ruby 已经诞生 20 多年了,如果没有这么多年来发生的各种事情与邂逅,根本不可能有今天这样的成绩。

进入创造编程语言的世界

大家有创造编程语言的经历吗?对于有过编程经历的人来说,编程语言是非常亲切的存在,但是他们往往会认为编程语言是现成的东西,也许谁都没有想过自己去创造一门新的编程语言。这也是情理之中的事情。

与人们说话用的语言(自然语言)不同,世界上所有的编程语言都是由某个地方的某个人创造的。它们不是自然产生的,而是根据明确的意图和目的被设计并实现的。所以,如果过去没有这些创造编程语言的人(编程语言的作者),那么我们今天可能还在用汇编语言编程呢。

在人们刚开始编程时,编程语言就随之出现了,可以说编程的历史就是编程语言的历史。

本书的目标是要你自己创造一门编程语言。可能有的读者会想:“现在再创造编程语言还有什么意义呢 ?”我稍后回答这个问题,现在我们先来看一下编程语言的历史。

个人创造编程语言的历史

早期的编程语言是由在工作中切切实实与编程语言打交道的人创造的,这些人大多就职于企业的研究所(比如 FORTRAN、PL/1 的发明)、大学(比如 LISP)以及标准委员会(比如 ALGOL、 COBOL)等。也就是说,设计开发编程语言是专业人士的工作,但是这个传统随着 20 世纪 70 年代计算机的普及开始发生了变化。一些计算机爱好者在拥有了自己的计算机后,出于兴趣开始编程,甚至开始开发新的编程语言。

其中最具有代表性的就是 BASIC 语言。BASIC 语言原本是美国达特茅斯学院用于教学的编程语言,它的语法非常简单,用极少的代码实现了最基本的功能,所以深受 20 世纪 70 年代编程爱好者的喜爱,并被他们广泛使用。

这些编程爱好者也开始开发自己版本的 BASIC 语言。当时,个人计算机 1 的内存顶多几千兆,他们开发的 BASIC 语言就是可以在内存如此之小的机器上工作的小规模版本。这些小规模的 BASIC 程序大小不到 1 KB,它们在 4 KB 左右的内存上也能工作,跟现在需要大内存的语言处理器比起来真是令人惊讶。

1通常称为微机。微机是微型计算机、微型机的简称。

微机杂志的时代

以个人开发的 BASIC 为代表的小规模语言(Tiny 语言)处理器不久便以各种各样的形式进行了发布。当时的软件有的以 Dump list 的形式刊登在计算机杂志上,有的将程序数据进行音频转换后收录在杂志附带的薄膜唱片(sonosheet)中发布。现在的人恐怕已经不知道薄膜唱片了吧。薄膜唱片是指塑料做的薄薄的唱片,不过唱片这个词几乎没有人用了。据说当时的计算机爱好者都用唱片播放器连接计算机来读取数据,而不使用磁带录音机这个最普遍的外部存储设备。

20 世纪七八十年代是计算机杂志(当时称为微机杂志)的全盛时期,在日本以下 4 种杂志竞争激烈。

  • RAM (广济堂出版)
  • My Computer (电波新闻社)
  • I/O (工学社)
  • ASCII (ASCII 公司)

这 4 种杂志中现在只有 I/O 仍在发行,不过也大不如前了。作为一个了解当时情况的人,我的内心充满了无限感慨。

这之后,My Computer 杂志派生出了 My Computer BASIC Magazine,又发生了很多事情,继续讲下去恐怕就会变成上岁数人的叙旧了,所以点到为止吧。如果去问问现在三四十岁的程序员,相信他们中间很多人都会眉飞色舞地讲起那个年代的事情。

当时的微机杂志附带了收录 BASIC 的薄膜唱片,除此之外还介绍了其他几个小规模语言,如 GAME、TL/1 等。这些语言都反映了当时那个时代的特色,非常有趣,我会在本节的最后对其进行介绍,请大家务必读一读。

个人创造编程语言的现状

为什么从 20 世纪 70 年代后期到 80 年代前期开始兴起个人创造编程语言了呢?我认为最大的原因是当时难以获取开发环境。

20 世纪 70 年代后期广泛使用的微机是 TK-80(图 1-1)那样的主板裸露在外的单板机,很多都是半成品,需要自己去钎焊。这样的机器不可能自带开发环境之类的东西,软件都要自己输入机器语言之后才会工作。

图 1-1 TK-80

20 世纪 70 年代末期才出现 PC-8001 和 MZ-80 那样的“成品计算机”。然而,这种计算机顶多带一个 BASIC 开发环境,因此人们很难自由地选择开发语言。虽说市面上也有商用的语言处理器,但 C 编译器的定价就要 19.8 万日元,这不是普通人可以轻易买得起的。于是,人们便有了热情去创造一门自己的编程语言。

可现在获取语言的开发环境已经不再是麻烦事了。各种编程语言和开发环境作为开源软件被公开,即使是非开源的,也可以轻松地通过网络得到免费版本。

这样一来,现在自己创造编程语言岂不是没有任何意义吗?如果这个问题的答案为“是”,那么本书在第 1 章开头就结束了。

我认为(而且为了这本书也应当这么回答),这个问题的答案为“否”。即使是现在,自己创造一门新的编程语言也是有意义的,而且有很重要的意义。

而且现在很多广泛使用的编程语言也都是在开发环境容易获取的情况下,由个人设计和开发出来的。如果个人开发编程语言真的没有意义,那么 Ruby、Perl、Python 和 Clojure 这些语言也就不会诞生了。

不过即便如此,我认为 Java、JavaScript、Erlang 和 Haskell 这些语言也可能会以其他形式出现,因为它们会作为业务和研究的一环被开发出来。

为什么要创造新的编程语言

那么如今个人设计开发编程语言的动力究竟是什么呢?回顾我自身的经历以及参考其他语言作者的意见,我认为有以下几点理由。

  • 提高编程能力
  • 提高设计能力
  • 打造个人品牌
  • 获得自由

首先,编程语言的实现可以说是计算机科学的综合艺术。作为语言处理器的基础,词法分析和语法分析也可以应用在网络通信的数据协议的实现等方面。

实现语言功能的库和实现其中的数据结构,这正是计算机科学要做的事情。尤其是编程语言的应用范围广泛,很难事先预测会被用于什么方面,因此库和数据结构的实现难度也就更大,但也变得更加有意思了。

另外,编程语言还是人与计算机间的接口。设计这样的接口,就需要深入考察人是如何思考问题的、下意识中有什么样的期待。反复进行这样的考察,对编程语言之外的应用程序接口(API)设计、用户界面(UI)设计,甚至用户体验(UX)设计都是有益的。

提升个人品牌

也许有人会感到意外,实际上在 IT 行业,对编程语言感兴趣的人不在少数。这是毋庸置疑的,因为编程与编程语言有着切不断的关系。以编程语言为主题的活动和会议等往往都会吸引很多人参加,由此我们也能感受到编程语言的魅力。正因如此,很多人在网上发现新的语言后就会开始尝试。就拿 Ruby 来说,它在 1995 年被发布到网上之后,仅仅 2 周左右就吸引了 200 多人加入邮件列表,着实令人惊讶。

可是,虽然有很多人愿意尝试使用新的编程语言,却几乎没有人会去设计并实现一门编程语言,而且是超越杂志提及的“小儿科语言”那种程度的能够实用化的编程语言。但我保证,仅凭设计出一个实用的编程语言这一点,你就会得到人们的尊敬。

在这个开源的时代,技术人要想生存下去,在技术社区的存在感是非常重要的。虽然技术人只要开源其软件就能达到站稳脚跟的效果,但编程语言的“特殊感”会进一步提升其品牌效应。

乐趣第一

另外,编程语言的设计与实现比任何事情都更有趣。的确如此。与计算机科学相关的具有挑战性的工程也是这样。设计编程语言还可以帮助使用这门语言的程序员思考,甚至左右他们的想法,这一点也非常有意思。

通常来说,编程语言有一种从别处获取的、不容侵犯的感觉。如果是自己创造编程语言,就完全没有这个问题。你可以按照自己的喜好进行设计,如果不满意或者有更好的想法,也可以自由地修改。从某种意义上来说,这是终极的自由。

编程在某种意义上是对自由的追求。通过亲自编程,我们可以获得单纯使用他人的软件时享受不到的自由。至少对我来说,这是编程的一个重要动机。于我而言,创造编程语言是获取更高程度自由的手段,也是我的乐趣与快乐的源泉。

为什么创造新编程语言的人不多

虽说自己创造一门编程语言有这么多好处,但并不是每个人都会去做。正如上文所说的那样,对编程语言感兴趣的人虽然有一些,但着手去创造编程语言的人几乎没有。说是“感兴趣的人有一些”,但从占总人口的比例来看,其实少到可以算作误差范围的程度,更不用说有动力去创造新编程语言的人了,就算没有也不足为奇。

我自己在关注编程语言几年后就着了迷,但是在进入大学主修计算机科学之后,才注意到并不是所有人都对编程语言感兴趣。这是因为我在偏僻的乡下长大,周围没有喜欢编程的人可供比较。这一点对我来说也不知道是幸还是不幸。

“难道我跟别人不一样?”意识到这一点的时候,我很震惊。因为当时的微机杂志上刊登了很多关于 TL/1 等编程语言的文章。我本以为对编程感兴趣的人(和我一样)很可能也会对编程语言着迷,但实际上并非如此。

本来就对编程语言不感兴趣的人自不用说,即使是感兴趣的人,也很难走到自己设计并实现编程语言这一步。

关于这个问题的原因,我思考过很长时间。作为编程语言设计者,在参加编程语言相关的活动时,我也曾以过来人的身份鼓励别人尝试一下,但结果总是不尽如人意。当然,万事开头难,开始一件新的事情是需要很大勇气的。但即使是这样,反响也太差了。

没必要想得很难

问了很多人之后,我才知道大家为什么不去着手尝试了。那是因为就算有兴趣创造一门新的编程语言,在开始之前多半也会有某种心理障碍,也就是觉得“编程语言有现成的,本来就不需要自己去设计和开发”。难得有那么几个人不会产生这种心理障碍,却又觉得语言的实现似乎很难。也就是说,他们觉得编程语言很有趣,自己也想做做看,却不知道如何去实现。

仔细想来,关于编程语言的实现的书虽然出乎意料地出版了很多,但大部分都是大学教材的难度,非常不容易理解。另外,与编译原理有关的“文法类型”和“Follow 集合”等晦涩的术语也频繁出现。

但是认真想一想,我们的目的是出于兴趣创造自己的编程语言,而不是去掌握编程语言的实现所需的所有知识。如果你认为在没有完全掌握正确的知识之前就无法着手创造编程语言,那就大错特错了,你的热情会被逐渐消磨殆尽。

成就一番伟大的事业首先需要的就是热情,不能保持热情是不行的。一旦有了创造编程语言的热情,就应尽快开始,以后再根据需要慢慢地掌握所需的知识即可。

本书主要介绍创造简单的语言处理器所需要的基本知识以及工具的使用方法,并不涉及编程语言实现的较难部分。相较于理论背景,我更想把重点放在如何设计编程语言上。

微机杂志中介绍的Tiny语言

GAME

GAME(General Algorithmic Micro Expressions)是由 BASIC 派生的 Tiny语言。它最大的特征是关键字全部是符号,以及所有的语句都是赋值语句。

例如赋值给“?”时会输出数值,反过来将“?”赋值给变量时会要求输入数值。字符串的输入输出使用“$”。另外,将行号赋给“#”时为 goto 语句,将行号赋给“!”时为 gosub语句(调用子程序)。

另外,像 "ABC" 这样的一个字符串语句会打印出字符串,后面有“/”的话会换行。

这是一门非常有意思的编程语言,示例代码如图 1-A 所示。它既像 BASIC,又不像 BASIC,请大家好好感受一下。

GAME 是一门非常简洁的语言,用 8080 汇编语言编写的解释器的大小还不到 1 KB。另外,由中岛聪(当时居然还是高中生)开发的使用 GAME 编写的 GAME 编译器,代码仅有 200 行左右。真不知道我们应该惊叹 GAME 的语言表现能力,还是中岛聪的技术能力。

TL/1

同一时期在 ASCII 杂志上发表的 Tiny语言中还有 TL/1(Tiny Language/1),它的名字应该是模仿了美国 IBM 公司开发的编程语言 PL/1。与受 BASIC 影响使用符号的 GAME 语言不同,TL/1 拥有类似于 Pascal 的语法,让人觉得更加“正常”。另外,TL/1 的语言处理器是编译型的,与主体为解释型的 GAME 比起来速度更快。但实际上 GAME 也有编译器,这一点我们在前文中介绍过。

TL/1 的特征是语法类似于 Pascal,以及变量类型只有 1 字节的整数。各位读者也许会想这样怎么编写代码,不过当时的主流 CPU 是 8 位的,所以 TL/1 设计成这样也不是很怪异。虽说是运行在 8 位 CPU 上,但包括 GAME 在内的其他语言都提供了 16 位的整数类型。

那么 1 字节无法表示的超过 255 的数值该如何编写呢?答案是按字节进行分割,用多个变量组合表示。比如用 2 个变量保存 16 位整数,边看计算溢出时的进位标志边计算(图 1-Ba)。在当时的 8 位 CPU 上,大部分处理用 16 位整数就已经足够了(地址用 16 位的话就可以访问所有地址空间)。作为 Tiny语言,这样的功能已经足够了。各位读者如果有兴趣,可以显式地查看进位标志,使用多个变量进行 24 位计算或 32 位计算。

可以处理指针和字符串

另外,指针也无法仅用 1 字节来表示。这里用 mem 数组进行访问,也就是说,用下面表达式中 hi,lo 表示的 16 位地址来访问指定地址的内容。

mem(hi, lo)

下面是将地址的值替换为 v

mem(hi, lo) = v

当时的个人计算机(微机)最多只有 32 KB 的内存,所以能用 16 位地址访问就已经足够了。

还有就是字符串。当然,我们也可以将字符串当作字节数组,对每个字节依次进行操作,但是这样处理太麻烦,因此 TL/1 设计了用于数据输出的 WRITE 语句。

例如,用 TL/1 开发的 Hello World程序如图 1-Bb 所示。TL/1 中变量本应只有 1 字节的整数,却出现了字符串。实际上,WRITE 是为了能够处理字符串而单独增加的语法。

WRITE 之外的语句是无法处理字符串的,所以不能进行普通的字符串处理,只能够操作 1 字节的整数。现在看来可能会觉得很不可思议,但是在不属于 Tiny 语言的 Pascal 和 FORTRAN 中,输入输出也是被特殊处理的,这在当时也许是一种比较普遍的做法。

100---------------- Comment -------------------
110|    如果紧接在行号之后的不是空白,则将该行作为注释
120--------------------------------------------
130
200 / " FOR循环语句 是 变量名=初始值,最终值  ... @=(变量名 + 步长) " /
210  A=1,10
220    ?(6)=A
230  @=(A+1)
240
300 / " IF语句的例子 " /
310  B=1,2
320    ;=B=1 " B=1 " /
330    ;=B=2 " B=2 " /
340  @=(B+1)
350
400 / " 数值输入与计算 " /
410  "A = ?" A=?
410  "B = ?" B=?
420  "A + B = " ?=A " + " ?=B " = " ?=A+B /
430  "A * B = " ?=A " * " ?=B " = " ?=A*B /
440
500 / " 数组与字符输出 " /
505--------令数组的地址为$1000
510  D=$1000
520  C=0,69
525--------作为2字节数组写入
530    D(C)=(C+$20)*256+C+$20
540  @=(C+1)
560  C=0,139
570--------作为1字节数组读取,并输出为字符
580    $=D:C)
590  @=(C+1)
600
700 / " GOTO 与 GOSUB " /
710  I=1
720  I=I+1
730  !=1000
731*  ?(8)=I*I
740  ;=I=10 #=760
750  #=720
760
900 / "程序结束 " /
910 #=-1
920
1000 / " 子程序 " /
1010 ?(8)=I*I
1020 ]

图 1-A GAME 语言的示例代码

% (a)
% 以"%"开始的行是注释,当时不能使用日语
BEGIN
  A := 255
  B := A + 2    % overflow
  C := 0 ADC 0  % add with carry
END

% (b)
BEGIN
WRITE(0: “hello, world”, CRLF)
END

图 1-B TL/1 语言的示例代码

 

时光机专栏

我原本打算改造 mruby

本节相当于从 2014 年 4 月刊开始连载的第一期内容。我热忱地讲述了编程语言的设计。

在第一期的时候,我还没有想好要设计开发一门什么样的编程语言。当时我打算改造一下自己编写的语言处理器 mruby,因此在连载时介绍了 mruby 源代码的获取方法以及代码目录结构等内容。不过在实际操作中完全没用上 mruby 的源代码,所以本书省略了这部分内容。

虽然本书不会涉及这部分内容,但由于 mruby 比较简单,所以它还是很适合作为编程语言的实现的教材来使用的。如果有读者想阅读 mruby 的源代码进行学习,可以从 http://www.mruby.org/ 这个网址开始各种尝试。另外,mruby 的源代码可以从 GitHub 网站(https://github.com/mruby/mruby)上下载。

如果在学习的过程中有什么疑问、意见,或者发现了错误,请通过 GitHub 的问题追踪器报告给我们。现在 mruby 的开发正朝着国际化方向发展,因此建议使用英语描述问题。另外,大家也可以通过我的推特账号(@yukihiro_matz)用英语或日语与我交流。

1-2 语言处理器的结构

本节将简单地讲解编程语言与语言处理器的关系以及语言处理器的结构,为开始设计编程语言做准备。我们首先会制作一个计算器程序,同时也将以mruby的实现为例,介绍一下实用的语言处理器。

虽说我们要创造一门编程语言,但具体要做什么,恐怕没有多少人能回答得上来吧。这是因为大部分人只去学习现有的语言,从未考虑过设计一门语言。

语言和语言处理器

编程语言拥有多层构造。首先,在大的层面上,可将编程语言分为表示交流规则的“语言”和处理此语言使其在计算机上运行的“语言处理器”。很多人在使用“编程语言”这个词时,往往都会将语言和语言处理器混同起来。

语言是由语法和词汇构成的。语法是一种规则,规定了在该语言中如何表述才能使程序有效;而词汇是能从使用该语言编写的程序中调用的功能的集合,之后会以库的形式逐渐增加。在设计语言的场景中说起词汇,就是指该语言一开始就具备的内置功能。

不知道大家有没有注意到,在定义语法和词汇的过程中并没有用到软件。构思设计“我心中的最强语言”是不需要使用计算机的。实际上,我在乡下读高中时,还没怎么掌握编程技术,但想着将来有一天或许自己要设计开发编程语言,就用自己瞎想的编程语言在记事本上写了很多程序。以前回老家时也找过那时候的记事本,但是怎么也找不到,估计是扔掉了吧,想想就觉得可惜。我也不记得是什么样的语言了,不过好像是受了 Pascal 和 Lisp 的强烈影响写出来的。

语言处理器是能够使语法和词汇在计算机上实际运行的软件。要想使编程语言成为真正的语言,而非仅仅停留在一个想法上,是离不开语言处理器的。无法运行的编程语言在严格意义上不能称为编程语言。

语言处理器的结构

当你打算制作语言处理器时,如果不了解语言和语言处理器究竟是什么结构,就无法实现你的创作愿望。方便起见,这里我们使用现成的语言处理器来介绍一下语言处理器的结构。我们先不拘泥于技术细节,来了解一下语言处理器的概况。

语言处理器是计算机科学的集合,是一款非常有意思的软件。计算机专业的大学生应该多少都学过语言处理器的制作方法,可以说语言处理器是计算机科学的基础(之一)。这就能够解释为什么有关语言处理器的图书比比皆是了。

但是很多介绍自制编程语言的方法的书,都将过多的笔墨用在了介绍语言处理器的制作方法上,几乎没有一本书涉及语言设计的相关知识。可能这些书所说的“自制编程语言的方法”就等同于“语言处理器的制作方法”,因此这么写也无可厚非。这些书的目的是教给你自制编程语言的方法(即语言处理器的制作方法),至于你是否真的会去制作,则不是它们要考虑的范围。

而本书将焦点放在了语言的设计上。不过,像曾经的我那样只是在记事本上写下自己空想的“理想语言”是没有现实意义的,因此我会先讲解一下语言处理器的基础知识作为导入。

首先我们来了解一下语言处理器的构成。

语言处理器的构成

语言处理器大体上可分为解释语法的“编译器”、相当于词汇的“库”,以及实际运行软件所需的“运行时(系统)”。这三大构成要素的比重会因语言和处理器性质的不同而发生变化(图 1-2)。

图 1-2 语言处理器的构成要素

早期出现的语言,比如 TinyBASIC 这样简单的语言,语法较少,编译器基本不做什么事情,主要的处理都在运行时完成。这样的处理器称为“解释器”(interpreter)(图 1-3)。

图 1-3 BASIC 语言处理器

编译器与运行时一体化的“解释型”的例子。在很多情况下,库也不单独分开

但是这样纯粹的解释型语言越来越少了。现在很多语言的处理器都是先将程序编译为内部代码,再在运行时执行内部代码。当然 Ruby 也是其中之一。这种“编译器+运行时”的组合形式,看起来像源代码未经转换就被直接执行了,因此有时也被称为“解释型”。

另外,像 C 语言这种在与机器非常接近的层面上追求效率的语言,乎不存在运行时,只有解释语法的编译器部分非常突出,这样的语言处理器被称为“编译型”(图 1-4)。语言处理器的构成要素与语言处理器自身的分类同名,这容易让我们感到混乱。在 C 语言这类语言中,作为转换结果的程序(可执行文件)是可以直接运行的软件,所以不需要负责运行的运行时。部分运行时的工作,比如内存管理等,由库和操作系统的系统调用负责。

图 1-4 C 语言处理器

输出可执行文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值