« May 2010 | Main | July 2010 »

June 29, 2010

区分一个包含汉字的字符串是 UTF-8 还是 GBK

今天检查 svn 仓库,发现又有同学没按规定提交包含汉字的代码。我们规律,所有源文件中包含的汉字必须使用 UTF-8 编码方式,而不能使用 GBK 。

总这么人工检查也不是个事。所以我想写一个 svn 的钩子,在提交前检查。在仓库的 hooks/pre-commit.teml 加一行检查脚本应该就可以了。

我想用正则表达式匹配一下,可是想了想又觉得 UTF-8 和 GBK 的编码集有点交集,不太好做。btw, google 了一下,的确有人写过特定编码的正则表达式

继续 google ,找到一篇跟我需求有点类似的文章UTF-8编码检测失败特例。看了正文,觉得不太靠谱,然后继续看回复,觉得这方法可行。

然后定睛一看,原来文章是孟岩写的,回复是我自己三年多前回复在他的 blog 上的。 -_-

打算还是自己写个小程序做检查,不用现成工具了。

具体算法复制回这里:

cloudwu 发表于2007-01-05 00:49:51 IP: 218.72.15.*

如果想区分一个完整的字符串是 GBK 还是 UTF8 其实蛮简单的。 虽然做不到 100% 有效,但也比上面的方法强许多。

UTF8 是兼容 ascii 的,所以 0~127 就和 ascii 完全一致了。

gbk 的第一字节是高位为 1 的,第 2 字节可能高位为 0 。这种情况一定是 gbk ,因为 UTF8 对 >127 的编码一定每个字节高位为 1 。

另外,对于中文,UTF8 一定编码成 3 字节。(似乎亚洲文字都是,UTF8 中双字节好象只用于西方字符集)

所以型如 110***** 10****** 的,我们一概看成 gbk/gb2312 编码。这就解决了“位”的问题。

汉字以及汉字标点(包括日文汉字等),在 UTF8 中一定被编码成:1110**** 10****** 10******

连续汉字数量不是 3 的倍数的 gb2312 编码的汉字字符串一定不会被误认为 UTF8 。用了一些gbk 扩展字,或是插入了一些 ascii 符号的字符串也几乎不会被认为是 UTF8 。

一般说来,只要汉字稍微多几个,gbk 串被误认为 UTF8 的可能性极其低。(只需要默认不使用 UTF8 中双字节表示的字符)可能性低,这里还有另外一个原因。UTF8 中汉字编码的第一个字节是 1110**** ,这处于汉字的 gb2312 中二级汉字(不常用汉字,区码从 11011000 开始)的编码空间。一般是一些生僻字才会碰上。

June 28, 2010

C 语言的前世今生

本篇是应《程序员》杂志约稿所写。原本要求是写篇谈 C 语言的短文。4000 字之内 。我刚列了个提纲就去了 三千多字。 -_-

现放在这里,接受大家的批评指正。勿转载。


C 语言的前世今生

C 语言,从 1970 年代设计并实现之初,它就注定了带有强烈工程师文化的语言,而缺乏一些学术气息。它的许多细节设计,都带有强烈的实用化痕迹。C 语言因 UNIX 操作系统而生,是 UNIX 系统的母语。这导致在这个广泛应用的操作系统上开发,必须通过 C 语言的形式和系统进行交互。这不仅影响了 UNIX 一个平台上的软件,既而也影响了后来世界上最大的桌面系统 Windows ,以及越来越多的嵌入式平台。

由于大部分应用软件最终都需要和操作系统打交道,所以用来开发应用软件的语言,绝大部分也需要利用 C 语言完成和操作系统的通讯。这个世界上绝大部分流行的编程语言,都选择了用 C 语言来实现其编译器或解释器,以及基础部分的运行时库。无论 C 语言设计本身有何种缺憾,在今天,它已无可取代。

到了今天,大部分程序员不再需要逐个时间周期的去抠程序的性能。不需要刻意追求速度最快,最节省系统资源的软件。不需要写那些和系统内核紧密联系的程序。但 C 语言在此之外,依然有其重要的应用领域。我们可以把它作为对最终机器模型的高层次的统一抽象工具,而不必考虑机器环境的差异。经过 30 多年的发展,证明了 C 语言的确是对经典机器模型的最佳表述。仅仅通过增加了一个非常薄的胶合层就得到了一个清晰简洁的设计。正是这一点,使得 C 语言在计算机硬件高速发展的几十年中,一直生机勃勃。

我们在讨论 C 语言时,其实不仅仅涉及了 C 语言本身那用三十几个保留字构成的精简的控制结构和简约的语言特征。还包括了一套对 # 号打头的预处理部分(尤其是基于文本替换的宏处理),以及某些惯用的源代码组织方式(例如:所有的接口定义被定义在后缀为 h 的文件中,并通过预处理方式替换进源代码),和基本的程序库。

这几部分语言核心之外的部分相对独立。以至于使用 C 语言开发并不一定使用标准化的那些东西。C 语言对运行时环境的依赖是非常小的。

而编译预处理器又使得语言富有弹性,甚至可以写出违背 C 语言哲学的代码。著名的 IOCCC 大赛展示了许多常人无法理解的 C 代码。但实际上,C 语言主张代码清晰,表里如一。开发者和维护者都能很容易的预测每一行代码背后的行为。避免存在一些阴暗的角落藏着一些罕见的用法导致程序运行时出现诡异的行为。C 语言在发展过程中一直坚持着最小意外原则。而这一点,正是 C 语言的一个著名发展分支 C++ 所偏离的东西。

C 语言并不是绝对意义上最快的语言。但是它的效率非常好,在切合大部分机器模型并给出统一抽象的基础上,几乎没有其它语言做的更好了。这也是 C 语言哲学的一部分:在统一硬件抽象模型的基础上,尽可能的利用所在硬件环境的一切资源。有时候,C 语言程序员会走向某种极端。追求语言细节的优化,觉得某种代码的组织方式会比另一种方式更高效。但几乎总是错的。优化取决于对具体硬件的理解,以及对编译器如何翻译这些代码的了解。但这正是设计 C 语言想避免的东西。我们不必去争论在语句级上每行代码精确开销的优劣。

同时,C 语言的另一设计哲学就是让每行 C 代码尽量准确的对应相当数量的目标机器码。这使得程序员可以更为容易的理解程序的运行过程。让程序员脑海里可以实时地做一个源代码到最终控制流程的映射。基于这个思想,C 语言一直没有增加对结构进行运算的操作符(而 C++ 中把类或结构模拟成原生类型的做法相当普遍)。甚至于 inline 关键字也迟迟没有被标准化(inline 出现在 C99 标准中,而这个最新的 C 语言标准并没有被广泛接受),正是因为它某种程度破坏了这一点。

C 语言在坚持以上几点理念时,并非突出某个方面(比如追求性能),而是同时兼顾的。

C 语言并不是这个世界上唯一的编程语言,可惜的是,不是所有程序员都认识到了这点。对于把 C 语言作为自己唯一开发语言的程序员来说,很有必要开拓自己的眼界,这样反过来才能更为清晰的理解 C 语言的内在精神。并不是说,某某语言本身是用 C 语言来实现,那么 C 语言就可以以同样的方式,解决那种语言解决的问题(甚至更为高效)。一些 C 语言中的概念,到了另一种语言中,很可能用完全不同的方式展现出来。正如自然语言会影响人的思维方式一样,编程语言一样会影响人对某种算法的编码形式。在 C 里,我们总以为某些写法是自然而然的,但换了种语言却很可能并不尽然。

无论如何 C 语言的语法和设计影响了许多其它语言。最为彻底的是 C++ 。以及大多数程序员都能叫的出名字的一些流行语言:Java , PHP ,Javascript,Perl ,C#,D,Objective-C 等等。 这些给人造成一种错觉,新的语言取代了旧的,对老的语言做了改良和完善。最广泛传播的观点是,C++ 是 C 的一个超集,它能做所有 C 能做的所有事情,且能做的更好。持有这种观点的 C++ 程序员们甚至向把已有的各种 C 代码用 C++ 重新实现。但实际上,C 和 C++ 更应该被看成是相互平等的存在。C++ 更像是一种借用了几乎全部 C 语法(但还是有细微差异)的全新语言。它们在很多方面都有设计理念上的差异。C++ 企图完全兼容 C 的语法却不想完全继承 C 语言的理念,这使它背负了巨大的包袱。而 C 的另一个继任者:Objective-C ,抛弃了一些东西,则显得清爽一些。

回顾 C++ 出现的时代背景在于把面向对象当成解决复杂问题的“银弹”的年代。这使得 C++ 在发明之初,迅速的占领了大量原本是 C 语言的市场,甚至被看成是 C 语言的替代品。但 C++ 的拥趸们并没有等到这一天。历史证明,面向对象也不是“银弹”、最近十年,C++ 的粉丝们从 C++ 语言的犄角旮旯里挖掘出来的各种武器,让 C++ 语言变成了包含多种编程范式的巨无霸。却并没有让解决问题变得更容易。这并不完全是语言的问题,可能有很大程度上是面向对象等开发方法本身的问题。这也证明了 C 语言保持自身的简洁正是其生机昂然的源泉。

和浩如烟海的 C++ 书籍相比较。如果你已经是程序员,但还不了解 C 语言的话。学习 C 语言,只需要读一本书,而这本书没有第二选择,就是经典的《The C Programming Language》(K&R)。薄薄的一本就讲透了语言的方方面面。可惜的是,C 语言过于注重对机器模型的抽象,并不适合用来程序员入门。尤其是在国内的教材市场,充斥着大量糟糕的 C 语言教材。在这些拙劣的教材中,甚至把开发工具(比如特定的 C 语言开发集成环境)和特定的硬件环境(甚至是过时的 8086 内存模型)与语言教学混为一谈。

对于 C 语言不是母语的程序员来说,有充分的理由去学习一下 C 语言。那是低投入,高产出的。它会使你学会在硬件层次上思考问题(这或许对你是一个新的思维角度)。而且 C 语言已经非常稳定,不会再有(它本身也不希望有)大的变化,不用担心学到的知识会过时。C 语言在 1990 年制订出一个现在通行的标准( C90 )以来,在 C 的主流开发社区中几乎没有变过了。虽然,从 1999 年开始,C 语言委员会几经修订 C 语言的新标准( C99 ),但似乎并不被广泛接受。虽然有很大程度上,这是源于世界上最大的 C/C++ 商业编译器提供商微软对其不感兴趣。可在开源界,即使有 GNU C 对 C 语言新标准的不断推动,那些实际用 C 语言做开发的大佬们还是纷纷表示,新的标准还不是很成熟。新的特性也不是特别有必要。

笔者用 C99 开发有一些年头,但也只使用了其中一个子集,不太敢在正式项目中完全推广。至于 C 语言近年来的发展,我个人比较欣赏苹果公司对 C 语言添加的 blocks 扩展以用来实现 closure 。但并不看好这些新特性会迅速融入 C 语言社区。

C 语言从语言角度上讲,最大缺陷在于要求程序员自己去做内存管理。用 C 语言去处理复杂的数据结构,程序员大部分的时间都花在了这上面,并且滋生了无数 bug 。调试 C 程序变成了一项独立于编写 C 程序的技能。防止缓冲区溢出、防止数据读写越界、正确的动态回收内存、避免悬空指针,这些在大部分语言看起来不可思议的关注点,在 C 语言程序员眼里变得稀松平常。甚至是衡量 C 程序员技能经验水平的重要标志。可要知道,这些和具体问题的解决过程无关。

也有人试图在 C 语言层面解决这个问题,例如以库形式提供垃圾回收的机制(笔者也曾做过类似尝试)。但 C 语言本身的设计使它无法成为一个完美的解决方案。同样的问题也存在于 C++ 。现在看来,不对语言做大的改造,很难回避。可改造本身又违背了 C 语言一贯的哲学。C 语言的发明人之一的 Ken Thompson 近年来参与了新的 Go 语言的设计和实现,可以看成从另一角度对新的程序开发语言的尝试,可那已经不是 C 。

这个问题在一定程度上也促使了 java 的诞生。Java 采用了虚拟机和字节码的方式改造了底层的机器模型。并在底层模型的基础上加入了垃圾回收机制。并在语言层面取消了指针。在 C 语言的原生地,也有更多的动态(脚本)语言出现。先是有 awk 这样的简易语言,后有 perl ,再是 python 等的流行。在 Unix 风格下,程序员倾向于为特定领域设计特定的语言。C 和 Unix 的设计哲学是一体的。它们都鼓励清晰的模块化设计。让模块之间独立,再用薄的胶合层联系起来。脚本语言在现代类 Unix 系统上大量出现,并充当这种粘合工作就是一种发展必然。而原本的充当粘合部分的脚本语言,也逐步发展起来,远远超出脚本的用途范畴。做为程序员,尤其是 C 程序员,必须对它们有所了解并掌握其中的一些,才能适应现代的挑战。

我们不应该指望一门语言解决所有的问题。可至于 C 语言本身,它将在很长的一段时间,带着它的优雅和缺陷,继续扮演它在计算机世界中重要的角色。


ps. 命题作文真难写啊。

June 13, 2010

把 vfs 实现好了

极尽简洁,然过犹不及(As simple as possible, but not simpler.)——爱因斯坦

这段时间的工作是把上次提到的 VFS 系统实现了。而写这篇 Blog 的促因是 twitter 上有同学想让我谈谈对 Linus 最近的一篇老生常谈的看法。哦,看似既然是语言之争。C 好,还是 C++ 好。但这次他平和了许多。Linus 惯有风格依旧,但少了些须三年前的争论 中的刻薄。

我想说,C 的三个特质(见引用文最后一段) 哪一点都不可忽略。Linus 这次强调的大约是第三点,也是 C++ 程序员们不屑一顾的一点。可对于多人协作构建的项目,这一点实在是太重要了。这并不是人人都聪明就能回避的问题。如果程序员们都足够睿智,反而更能意识到沟通之成本。其实即使是你一个人在做整个项目,从前的你和现在的你以及将来的你,同样有沟通(记忆)的成本。人不可能两次踏进同一条河流。

我的观点在于,如非必要,勿增概念。这是我这次翻新资源管理系统的初衷。在项目组内,这种大的修改是反对多过赞同的。我尚无能力像说服自己那样说服每个人。虽然我及其主张项目演化中的民主,但这一次稍显独断,实在是不得已。因为我觉得这是个不明显的重大缺陷。虽然老的设计它精巧,且可以很好的工作,但它不适合长期保留。

理想的大项目,应该是每个人专心做自己的一块东西,它涉及的外部部分用极少的文档或易于表达的概念定义清楚:无论是程序接口、对资源的占用、适用的范围等等。尤其是弱化 framework 这种联系方方面面的巨无霸。

说回这次实现的 VFS 模块。实现还是比较简单的。但是设计很困难。难点在于,虽无可避免的有一些 framework 的倾向(比原来的系统要弱化许多),但怎样让后面具体的文件系统跟这个小型 framework 交流最少。架子主要解决的是内存资源管理问题和用 cache 提升索引性能的问题。

我定义了两个内部数据结构,借用 linux 的 vfs 中的概念表达。一种叫 dentry 描述目录项,一种叫 inode 描述文件项。但没有暴露这两个数据结构的内部布局。用户扩充的时候,需要给出各一个额外的数据指针来扩展自己的结构。这种手法,我曾经描述过。实际应用的时候,没有定法。

所有函数都应该是可重入的,但暂不要求线程安全。以此实现文件系统的嵌套 mount 。起初,我认为实现一个 zipfs 会很容易。可以 mount 上通用的 zip 文件使用。实际实现时,发现无论是通用的 zlib 还是另一个使用执照稍微麻烦一点的 zziplib ,都不提供高效的 seek 接口。我花了一整个晚上研究 zip 的文件格式和解码算法。发现对于这种流式数据压缩,很难做到特别高效的 seek 算法。

利用 zlib 的底层 api ,我想了个办法来提高 zlib 中带的 minizip 库的 seek 效率(目前必须通过假读来实现),不过也不可能达到 O ( LogN ) 的水平。所以我放弃了这个打算。没有高效的 seek 接口,坏处在于嵌套的 mount zip 文件中被打包的 zip 文件性能会很差。当然,可以选择在 zip 被打包进 zip 时不压缩,这样稍微改造一下 minizip ,就能把 seek 提高到 O(1) 的水平。我权衡了一下,还是把此类需求留到以后自己设计一个新的包格式为佳。(采用分块压缩即可)

最终我实现了一个基本的 rootfs ,一个 memfs 用于在内存中创建文件和目录(主要用于创建出最初的 mount 点),一个 nativefs 用于把本地文件系统上的目录树以只读方式映射给引擎,一个 zipfs 实现开发期基本的打包方案。未来可能会增加一个可写的文件系统,用于保存一些本地设置。一个自定义的包方案。

同时在同事的协助上,把 engine 中老的资源操作的接口迁移到新系统上。

btw, 我还考察了一个修改过的 unrar 的库。不过由于繁杂的执照问题,暂时没有采纳。如果人力充足的话,日后倒可以找人来加上。

June 11, 2010

有关 Forth

今天晚上继续读 《Masterminds of Programming》,忍不住又翻译了半章关于 Forth 之父的访谈。我以前读过几篇更早时期关于他的访谈,部分了解他的观点。小时候还特别迷 Forth 。这位神叨叨的老头很有意思。

没看过原来的译本,只是自己按自己的理解翻了第 4 章 Forth 的前一半。我也算对 Forth 很有爱的人吧,也还了解 Forth 里诸如 ITC (Indirected-threaded code) 这种术语到底指的什么,不过还是觉得翻译有点吃力。

对 Forth 同样有爱的同学们姑且看之吧。


Forth 语言及语言的设计

你怎样定义 Forth

Chunk: Forth 是一门计算机编程语言,其语法规模简到极致。把参数栈专门独立出来是语言的一大特色,这使得子程序调用非常高效。因此,语言采用后缀表达式(先列参数,再跟操作符),并鼓励把程序高度细分成众多短小的子程序的风格。这些子程序共享栈上参数。

我曾经读过关于 Forth 这个名字的介绍,据说是象征着第四代软件开发。你可以给我们多介绍一些吗?

Chunk: Forth 源于 "Fourth (第四)" 这个词。暗指 “第四代计算机语言”。据我回忆,我跳过了一代。FORTRAN/COBOL 是第一代;Algol/Lisp 是第二代。这些语言都强调语法。语法越详尽,越能检查出错误。但大部分错误是语法错误。我决定将语法元素减到最小,而强调其语义。被加载的 Forth 词就真正的表达其含义。

你把 Forth 当成一个语言工具集。我这样理解这个视角:给出相对其它语言更简单的语法,提供比其它语言所用的更短的词构成一个词汇表的能力。我还漏掉了什么吗?

Chunk Moore: 就是这样。其中关键一点是,我们尽其可能的分解。一段 Forth 程序有大量的小词构成。相对来看,同样的 C 程序用到的词则少的多,而且每个词都要大的多。

我所谓短小的词,是指可以用一行源代码定义出来的词。你可以用刚定义出来的词来定义下一个新词,如此层叠,最终得到数千个词的定义,最终形成了语言。这里面临的挑战是:1) 判断哪些词是有用的。2) 记住所有定义出来的词。我现在正在编写的应用程序有上千个词。所以我写了个工具来搜索这些词,不过你必须先记得有这个词,大概怎么拼才找得出来。

好了,现在有了种完全不同的编程风格,程序员需要一点时间才能适应。我已经看过太多 Forth 程序就像是从 C 程序直译过来的。这样做可不对。正确的做法是从零开始。关于这个工具集还有一个有趣的地方,只要你好好做,定义出来的新词和内核中预定义的词并无不同。它们一样高效,对外的意义也相同。定义你自己词是没有额外开销的。

外部结构看起来是由许多小词组成,这一点是源于 Forth 的实现吗?

Chunk: 这是我们非常高效的子程序调用序列的结果。这里面没有参数传递,因为语言是基于栈的。所有的一切仅仅只是调用和返回。而栈暴露在外。让机器识别的语言编译在一起,进入和退出字程序只需要逐字翻译成一条 call 指令和一条 return 指令。你总可以进一步的达到汇编语言层面,得到等价的指令。你可以定义一个词刚好执行一条真正的机器指令,而不是做一次子程序调用,这样,它可以匹敌任何其它语言的效率,甚至比一些语言更高效。

这样就没有 C 调用的消耗

Chunk: 没错。这赋予了程序员巨大的灵活性。如果你能聪明的分解问题,你不仅仅只是高效的完整任务,还使得完成的过程变得格外易读。

另一方面,如果你做的很糟糕,你最终的代码世界上只有你一个人能解读——连你那全知全晓的经理都看不懂你写的代码。你可以写出天书。所以,这是把双刃剑。你能做的完美无暇,也能弄得污七八糟。

你能说点什么(或展示点代码)让用别的编程语言的开发人员对 Forth 一见钟情吗?

Chunk: 让有经验的程序员对 Forth 感兴趣挺难的。这是因为他已经为他正在使用的语言/操作系统相关的工具学习做了大量的投资。为他的应用程序积累了许多。即使是告诉他们,Forth 会更小,更快,更简单,也及不上把所有东西都重写的代价。一个新手程序员,或是一个没这么干扰的需要写点代码的工程师可能更容易接受一点。嗯,或是有某个有经验的程序员要开个新项目,而这个项目有些新的约束条件,比如我现在在做的多核芯片上做开发。

你提到你见过的大量 Forth 程序看起来像 C 程序。那么你自己怎么设计一个更好的 Forth 程序的?

Chunk: 由底至上。

首先,你估计会有一些 I/O 信号要去产生,好吧,就来产生它们。这样你就写一些代码控制这些信号的产生过程。然后你一点点来,知道最终构建出高层的词。假定你把它起了个名字叫 go ,接下来你敲一下 go ,一切如期发生。

我不信任由顶向下的系统分析方法。他们判断问题是什么,然后再分解,分解出来却很难实现。

领域驱动设计建议以客户的词汇来描述商务逻辑。在创建词库和使用你问题域的术语之间有什么联系?

Chunk: 最好是程序员在开始写代码前了解那个领域。我会和客户沟通。我会倾听他用的词汇,并试着用这些词,这样他也能明白程序在做什么。Forth 因其采用后缀记号法使代码费用易读。

如果我写一个经济有关的应用程序,我可能会用一个叫作 "percent" 词。你可以在代码中写 "2.03 percent" 。这里 percent 的参数就是 2.03 ,一切都和看起来那样自然。

一个从穿孔卡片计算机时代开始的项目居然到了互联网时代的现代计算机上还这么有用。Forth 于 1968 年在 IBM 1130 上设计并运用。它到了 2007 年继续为并行处理所用,这真奇妙。

Chunk: 其间 Forth 也进化了。不过 Forth 可能是最为简洁的计算机语言。它没给程序员附加任何约束。他(她)可以瘦的层次化方式定义一些词精确切合问题的原貌。

你在设计程序时,是否把让程序读起来像英文一样做为一个目标?

Chunk: 在非常高的层次上看,是这样。但英文并非功能性描述的好语言。英语不是设计来干这个的,不过英语和 Forth 有个共同的特性,就是你能定义新词。

你用以前定义好的许多词通过解释的方式来定义新的词汇。对于自然语言来说,有可能不严谨。如果你查字典的话,会发现有些是循环定义,你查不到本质内容。

是不是把注意力转移到一堆词上面,比 C 里出现的那些各种括号来说,Forth 因此能写出更好味道的程序?

Chunk: 希望如此。这使得 Forth 程序员去关注事物的外貌,而不仅仅是其功能。如果你能组织起一系列的词,把它们有序的排列起来,会感觉很好。这正是为什么我开发了 colorForth 。我曾为 Forth 里老的语法烦恼。比如,按现在的方法,你要做注释,就必须用一左括号和一右括号来括起来表示。

我看着所有标点符号说,“好吧,可能还有更好的方式。” 这更好的方式最麻烦的一点是,源代码中每一个词都需要附加一个标签。如果我能忍受这点开销,所有的符号都另人舒服的消失了,取而代之的是,每个词都有了颜色。对我来说,这是个优雅的表达其功能的方式。

我遭到了色盲群体的抨击。他们对我试图把他们排出程序员行列的做法义愤填膺。不过某人最后想了个招,用字体的区分代替颜色的区分,这也是个不错的方法。

关键点在于,每个词有个四位的标签,这能区分出 16 种事物。编译器可以立刻感知它要做什么,而不用从上下文去推断。

第二代和第三代语言都皈依了极简主义,举例来说就是实现了 meta-circular bootstrapping (圆环自举,靠自身把自身运作起来)。Forth 是在对语言的概念定义以及硬件需求量方面的极简主义的最好的例子。这是当年的时代特征或是你做出的跨时代的创举吗?

Chunk: 非也。当时再三考虑的设计目标是尽可能的做一个最小的内核。只预定义最为必要的几个词,然后让程序员再去添加他觉得合适的。主要因素是可移植性。在那个时代,大打的小型计算机,接着又是一大坨微型计算机。而我必须把 Forth 弄到如此之多的机器上去。

我想干这点事能尽量轻松点。我要干的就是弄出一个百来个词的内核,以此能够组成一个,我叫作操作系统,但其实不完全是操作系统的东西,这个东西再给出几百个词为人所用。接下来你就能在这上面做开发了。

我来提供做前两个阶段的工作,让程序员去做第三个。我也经常做应用程序开发程序员。定义我知道的词总是很有必要。头一百个词可能用机器语言或汇编语言定义,至少是直接和特定平台打交道。第二和第三百个词可以是高层次的词,在较低层次最小化机器依赖性。接下来,应用程序就能最大限度的做到机器无关了,这样很容易把程序从一台机器移植到另一台上。

当初你能在第二阶段之上方便的移植吗?

Chunk: 绝对如此。比如我有个文本编辑器,用来编辑源代码的。它总是在各种机器上不需要修改任何地方都能用。

坊间流传着一个传说,每次你看到一台新机器,你就立刻动手把 Forth 移植到上面。是说的这个吗?

Chunk: 没错。实际上对于理解一台机器如何工作;了解那些可以用来更容易实现 Forth 标准包里词的诡异机器特性;这是条最简单的途径。

你是怎样发明 indirect-threaded code 的?

Chunk: 代码是一个很微妙的概念。每个 Forth 词在字典里有一个入口。对于基于直接线索的编码(direct-threaded code ,有译为直接串线编码),遇到引用每个词的位置直接指向要执行的代码。而基于间接线索的编码(indirect-threaded code 有译为间接串线编码) 则指明一个包含了代码所在地址的位置。这使得地址之外的信息能被访问到——比如,一个变量的值。

这可能是最为紧凑的词组织方法了。它可以等价于基于直接线索的编码和基于子程序的编码(subroutine-threaded code ,即类似 C 语言编译成的那种直接调用子程序方式)。当然这个概念和术语在 1970 年时还没有。但对我来说,这是实现各种各样词的最自然方式。

Forth 会如何影响计算机系统的未来走向吗?

Chunk: 这已经在发生了。我在微处理器优化方面干了 25 年,最新近的一个多核芯片的核心是 Forth 计算机。

Forth 提供了些啥?作为一个简单的语言,它使得这样一台简单的计算机:有 256 个字的本地内存;两个下压栈;32 条指令;异步操作;易于和相邻机器通讯;可以很小,且功耗极低。

Forth 鼓励被高度分解的程序。这非常适合在多核芯片上做并行处理。大量小程序鼓励你每一个都深思熟虑的设计。这样最终你可能只需要写 1% 的代码就够了。

只要我听到有人吹嘘代码达到了上百万行,我就知道他们肯定前面理解错问题了。当代没啥问题需要写几百万行代码。要么是程序员太粗心、要么项目经理太混蛋、要么就是为兼容一些不存在的需求。

使用 Forth 对需要小计算机编程是个巨牛叉的策略。别的语言都提供不了相当的模块化能力和扩展性。尤其计算机越来越小,它们之间必须做网络化协作(智能微尘?),这就是未来的环境。

这听起来像 Unix 的重要原则之一:以许多程序,每个只做一件事,相互作用。这依旧是当今最好的设计吗?在一台机器上跑多个程序会被通过网络运行的多个程序取代吗?

Chunk: 让代码跑在多个线程上的这个概念,被 Unix 和别的操作系统所实现。这是并行化处理的先驱。但这里有些重要的差别。

大的计算机承担得起多线程通常会要付出的一些代价。最终弄出个庞大的操作系统。对于并行化处理来说,永远都是计算机越多越好。

在资源一定的情况下,更多的计算机意味着更小的计算机。但很小的计算机是承担不起在大计算机上承担的代价的。

小的计算机的网络化会发生在芯片之上,通过 RF 连接的芯片之间。小的计算机内存也小。操作系统无处容身。计算机必须自治,自己要有能力保持通讯。因此通讯环节必须简单——没有那些煞费苦心的协议。软件也必须紧凑高效。最为理想的应用程序就是 Forth 了。

那些需要数百万行代码筑就的系统将会淡出历史舞台,是它们造就了巨大的中央计算机。分布式技术需要条不同的思路。

一门语言若是设计成支持繁杂的,拘泥于语法条条框框的代码,就会鼓励程序员写出巨大的程序。并以此自得,洋洋得意。没什么压力去寻找紧凑的方案。

以繁杂句法定义出来的语言生成的代码也可以很小,但通常做不到。以语法默示的实现流程导致了笨拙而不那么高效的目标代码。这对于小的计算机来说并不合适。一门良好设计的语言在源码和目标码之间存在一对一的联系。这向程序员昭显了源码如何生成为最终代码。注重性能、减少对文档的需求,使得程序员感到满足。

Forth 设计成对于源代码和二进制输入的目标代码皆很紧凑,这也是嵌入式开发中广泛使用的原因。不过在许多其它领域程序员总有别的理由去用别的语言。是不是说这些语言的设计的某些方面只是增加了原代码或是目标码的开支吗?

Chunk: Forth 的确很紧凑。一个因素在于他的语法量很小。

其它语言看起来是故意的增加一些语法,弄出点冗余,可以帮助语法检查以及错误检测。

Forth 没提供啥机会用来做错误检查,因为它没有冗余信息。这也使得源代码非常紧凑。

我感觉其它语言几乎所有的错误都出在语法上。设计者看起来为程序员犯下编译器就都能找出来的错误创造了条件。这没啥经济价值。这不是为写出正确的代码自找麻烦吗。

比如说类型检查吧。不同的数字类型之间的赋值错误会被侦测到。无意中带来的后果是程序员必须自己来转换类型,有时就是想回避类型检查而得到他们真正想干的事情。

语法导致的另一结果是它必须适应所有的应用程序的意图。这就会越来越复杂。Forth 是一个可扩展的语言。程序员可以创建一些别的语言只能通过编译器的改进才能获得同样性能的结构。而所有的能力不需要一开始就想好提供出来。

Forth 的特征之一是使用后缀操作符。这能简化编译器,从源代码到目标代码给出一对一的关系。程序员对他写的代码的充分理解能增加代码编译后的紧凑程度。

**许多最近的计算机语言(尤其是 Python 和 Ruby)都把可读性引为其关键好处。Forth 在这方面与之相较可以从中学到并保持些什么?Forth 能在可读性方面的定义方面传授给其它语言些什么东西?

Chunk: 计算机语言都宣称要可读。但他们并不可读。或许对懂这门语言的人来说是可读的。但初学者还是稀里糊涂的。

问题就在于晦涩、武断、隐秘的语法中。那些小括号啦,& 符啦,等等。你试着学习它们为啥出现在那里,最终推断,其实没什么好理由。但是你还是要按规则办事。

你无法说这门语言。你必须像 Victor Borgia 那样叽里呱啦的把符号都念出来。

Forth 通过最小化语法来减轻这个问题。它用的哪些个神秘符号,@ 和 ! 可以读成“取”和“存”。这些个使用符号是因为出现的太频繁了。

程序员被鼓励使用自然语言中的词汇。它们不通过标点的间隔组织在一起。若是你选好了词,你就能构造出有意义的句子。实际上有人用 Forth 来写诗。

另一个优势是后缀表示。一个像 “6 英寸”这样的短语能把操作符“英寸”作用于参数 6,这是非常自然的表达方法。非常的可读。

另一方面,程序员的任务就是开发吃一组词来描述问题。这个词典会变得非常大。读者需要了解整个词典使得程序可读。程序员就必须好好的定义词。

总而言之,用任何语言,这些都会影响程序的阅读。

你如何定义你的工作中如何定义成功?

Chunk: 一个优雅的解决方案。

人们并没有用 Forth 编程。Forth 就是一个程序。他们添加新的词创建一个词典来定义问题。当正确的词被定义出来,一切都浑然天成。接下来你就能你可以互动地解决所有相关问题的任何方面。

举个例子:我可以定义一些词来描述一个电路。我以后想把这个电路加到一块芯片里去,显示电路的布局,校验设置规则,模拟跑一下。用来干这些事情的词决定了应用程序的形态。如果精心选择这些词,提供一个紧凑而有效的工具集,然后我就搞定了。

你在哪学会写编译器的?在那个年代每个人都必须去写编译器吗?

Chunk: 哦,我在六十年代去了趟 Stanford ,那里有组研究生正在写一个 ALGOL 编译器——Burroughs 5500 用的版本。当时我想他们不过才三四个人,就三四个人坐在那写一个编译器,我那个内牛满面啊。

我想,“靠,他们要能做,我也能做。”然后我就干了。其实一点也不难。当年写编译器还是有点神秘西西的。

现在其实也还是神秘西西的

Chunk: 嗯,不过已经没那么神秘了。你看现在新语言一个个的冒出来,我不知道算解释型的还是编译型的,不管怎样,有黑客风范的人都想做一个。

操作系统是另一个稀奇古怪的东西。操作系统吓人的复杂,而且完全没用。它是 Bill Gates 成功向世界推销出去的一个光彩夺目的概念。这可能是世界上出现的最大的骗局。

操作系统对你来说做着绝对的无用功。其实你有这样一些东西就够了:一个叫作磁盘驱动程序的子程序,一个叫什么什么通讯支持的子程序,而在现代社会,操作系统啥也没做。实际上,Windows 花了大量时间在包装层上,或是诸如磁盘管理器这样不相干的东西上。你有了上 G 的磁盘,有了上 M 的内存。世界格局发生了变化,使得操作系统不那么有用了。

那设备支持怎么办?

Chunk: 你对每个设备有一个子程序。那是一个库,而不是操作系统。你需要那个就装载哪个。

在工作间隙后,你怎么继续编程?

Chunk: 在被困扰的时候,我不会中断我的编码。我会充满热情的思考问题,做梦都会想着它们。我想这是一个 Forth 的特质:在小段时间(以天计)内全神贯注的解决一个问题。这帮助 Forth 应用程序自然的被分解为一个个子项目。几乎所有的 Forth 代码都简单易读。当我真的要做一些偏激的事情,我会做好注释。好的注释能帮我以后回到问题中,不过重新阅读和理解代码总还是有必要的。

你在设计或编程中犯过最大的错误是什么?你从中学到点什么?

Chunk: 20 多年前,我想为设计 VLSI 芯片开发个工具。我的新电脑上没有 Forth ,因此我就想用另种方案,写机器语言。不是汇编,就是用 16 进制码敲机器指令。

我像我写 Forth 程序那样编写代码,分层次的定义出需要简单的相互有关的词。最终搞定了。我用了这个东西 10 年。但是无法维护,也没有文档。最终,我用 Forth 重写了一遍,这个玩意变得更为小巧而且简单多了。

我的结论是,Forth 比机器语言更高效。一部分源于其交互性,另一部分是因为它的语法。Forth 的一个很漂亮的方面是,数字可以用计算它们的表达式文档化的表达。

June 09, 2010

采访 Lua 发明人的一篇文章

《Masterminds of Programming: Conversations with the Creators of Major Programming Languages》是本相当不错的书。博文翻译出版了这本书,中文名叫做《编程之魂》。

书是好书,可惜翻译这本书需要对各种语言的深入研究,看起来译者有点力不从心。出版社打算重新做这本书。受编辑所托,我校对了其中第七章:有关 Lua 的一段。原文读下来拍案叫好。可惜译文许多地方看起来有些词不达意。许多在口语化交流中提到的术语被忽略了做了错误的翻译。有些部分应该是对 lua 理解不够而没能表达清楚。

仔细校对了两段后,我干脆放弃原译本,自己动手翻译了一份(保留了不到 1/4 原来的译文)。虽然个人能力有限,但也算是每句话自己都看明白了再译的。虽说有些地方没有直译,但也算没有夹带私货。

这里贴出一段,希望大家阅读愉快。


7. Lua

Lua 是一门非常之小,但五脏俱全的动态语言。它由 Roberto Ierusalimschy、Luiz Henrique de Figueiredo 和 Waldemar Celes在1993年创建。Lua 拥有一组精简的强大特性,以及容易使用的 C API ,这使得它易于嵌入与扩展来表达特定领域的概念。Lua在专有软件界声名显赫。例如,在诸多游戏中,比如 Blizzard(暴雪)公司的《魔兽世界》和 Crytek GmbH 公司的《孤岛危机》,还有 Adobe 的 Photoshop Lightroom ,都使用它来作脚本 和 UI 方面的工作。它继承了 Lisp 和 Scheme,或许还有 AWK 的血脉 ; 在设计上类似于 JavaScript、Icon 和 Tcl。

7.1 脚本的威力

你是如何定义 Lua 的?

LHF:一种可嵌入,轻量,快速,功能强大的脚本语言。

Roberto:不幸的是,越来越多的人们使用“脚本语言”作为“动态语言”的代名词。现在,甚至是 Erlang 或者 Scheme 都被称为脚本语言。这非常糟糕,因为我们无法精确的描述一类特定的动态语言。在最初的含义解释中,Lua 是一种脚本语言,这种语言通常用来控制其它语言编写的其他组件。

人们在使用Lua设计软件时,应该注意些什么呢?

Luiz:我想应该是用 Lua 的方式来做事。不建议去模拟出所有你在其它语言中用到的东西。你应该真的去用这个语言提供的特性,我想对于使用任何一门语言都是这样的。就 Lua 来讲,语言的特性主要指用 table 表示所有的东西,用 metamethod 做出优雅的解决方案。还有 coroutine 。

Lua 的用户应该是哪些人呢?

Roberto :我认为大多数没有脚本功能的应用程序都能从 Lua 中受益。

Luiz:问题在于,大多数设计者很长时间都不会意识到有这种需求。当已经有了诸多用 C 或 C++ 编写的代码,为时已晚。应用程序设计者应该从一开始就考虑脚本。这会给它们带来更多的灵活性。而且这样做还可以更好的把握性能问题。因为这样做以后,会迫使他们去考虑程序中到底哪里是性能关键,而哪些地方无伤大雅。而这些性能不太重要之处,就交给脚本去处理,开发周期短,速度快。

从安全性的观点来看,Lua 能为程序员提供些什么呢?

Roberto:Lua 解释器的核心部分被构建为一个 “独立的应用程序(freestanding application)”。这个术语来自 ISO C,大意是说,这部分不使用任何跟外部环境有关的东西(不依赖 stdio、malloc 等)。所有那些功能都由扩展库来提供。使用这种体系结构,很容易让程序限制对外部资源的访问。具体来说,我们可以在 Lua 自身的内部创建出一个沙盒,把如何我们认为危险的操作从沙盒的外部环境中剔除。(比如打开文件等)

Luiz:Lua 还提供了用户自定义的调试钩子,用它可以监视 Lua 程序的执行。这样,在 lua 中运行时间过长或是使用了过多内存的时候,我们可以从外部中断它的执行。

Lua 有什么局限性?

Roberto:我认为 Lua 的主要局限是所有动态语言共有的。首先,即使是利用最先进的 JIT 技术(Lua 的 JIT 是所有动态语言 JIT 中最好的之一)也达不到优秀静态语言的性能。其次,一些复杂的程序从静态分析中受益匪浅(主要是静态类型)。

是什么促使你决定使用垃圾收集器?

Roberto:Lua 从第一天开始,就一直使用垃圾收集器。我想说,对于一种解释型语言来讲,垃圾收集器可以比引用计数更加紧凑和健壮,更不用说它没有把垃圾丢得到处都是。考虑到解释型语言通常已经有自描述数据(通过给值加上标签之类的东西),一个简单的标记清除(mark-and-sweep)收集器实现起来极其简单,而且几乎对解释器其余的部分不会产生什么影响。

对于无类型语言(untyped language),引用计数会很重量。没有静态类型,每次赋值都可能会改变计数,对变量的新值和旧值都需要进行动态检查。后来尝试过在 Lua 中引入引用计数,并没有提高性能。

你对 Lua 处理数字的方式满意吗?

Roberto:从我的经验来看,计算机中的数字老是会给我们带来一些意外(因为它们也来至于计算机之外!)。至于说 Lua 使用 double 作为唯一的数字类型,我认为这是一种合理的折衷方案。我们已经考虑了很多其他可选方案,不过对于 Lua 来说,这些方案要么太慢,要么太复杂,要么太耗内存。对于嵌入式系统,甚至使用 double 也不是一种合理的选择,因此,我们可以使用一个备选的数值类型,比如说 long ,来编译解释器。

你为什么选择 table 作为 Lua 中的统一数据结构?

Roberto:从我的角度,灵感来自于VDM(一个主要用于软件规范的形式化方法),当我们开始创建 Lua 时,有一些东西吸引了我的兴趣。VDM 提供三种数据聚合的方式:set、sequence 和 map。不过,set 和 sequence 都很容易用 map 来表达,因此我有了用 map 作为统一结构的想法。Luiz 也有他自己的原因。

Luiz:没错,我非常喜欢 AWK ,特别是它的联合数组。

程序员可以从 Lua 中的 first-class 函数中获得怎样的价值?

Roberto:50多年来,虽然名称各异:从子程序到方法,“函数” 已经成为编程语言的主要部分,因此,对函数的良好支持为所有语言必备。Lua 支持程序员使用函数式编程领域中的一些功能强大的技术,比如,把数据表示成函数。例如,一种形状可能用函数来表示,给定 x 和 y ,可以判断这个点是否在这个形状内。这种表示方式可以用于一些操作,比如联合和交集等。

你为什么要实现闭包 ( closure ) ?

Roberto:闭包自始至终我们都想在 Lua 中实现:它简单、灵活、功能强大。从第一版开始,Lua 就把函数做为一等值 ( first-class value ) 对待。这被证明非常有用,即使是对于没有函数式编程的“常规的”程序员来说也是一样。而不支持闭包的函数,其实用价值就会大打折扣。顺便说一句,闭包这个术语来源于一种实现技术,而不是指它本身的特性。从特性描述上来说,闭包相当于“带词法作用域的一等函数”,当然用闭包这个术语更为简短。

你打算如何处理并发问题?

Roberto:我们不信任基于抢占式内存共享的多线程技术。在 HOPL 论文中,我们写道:“我们仍然认为,如果在连 a=a+1 都没有确定结果的语言中,无人可以写出正确的程序。” 我们可以通过去掉抢占式这一点,或是不共享内存,就可以回避这个问题。而 Lua ,提供用这两种方式解决问题的支持。

使用协程(coroutine),我们可以共享内存,但不是抢占式的。不过这个技术利用不到多核机器。但在这类机器上,使用多“进程”就能极大的发挥其性能。这个我提到的“进程”是指在 C 里的一个线程,这个线程维护自己独立的 Lua 状态机。这样,在 Lua 层面上,就没有内存共享使用。在《Lua 程序设计第二版》[Lua.org] 中,我给出了这种方式的一个原型。最近我们已经看到有些库支持了这种方式(比如 Lua Lanes 以及 luaproc)。

你没有支持并发,但你为多任务实现了一个有趣的解决方案:非对称式协程。它们如何工作的?

Roberto:我有一些 Modula 2 语言的经验(我的妻子在她的硕士论文工作中为 M-code 编写了一个完整的解释器),使用协程作为协作式并发以及别的控制结构的基础设置是我一直偏爱的方法。然而,Modula 2 中那种对称式协程,在 Lua 中行不通。

Luiz:在我们的 HOPL 论文中,对那些设计决策全部做了极为详细的解释说明。

Roberto:我们最终选择了非对称式模型。它的基本思想非常简单。通过显式调用 coroutine.create 函数来创建一个协程,把一个函数作为协程主体来执行。当我们启动 (resume) 协程时,它开始运行函数体并且直到结束或者让出控制权 (yield) ;一个协程只有通过显式调用 yield 函数才会中断。以后,我们可以 resume 它,它将会从它停止的地方继续执行。

它的基本思想非常类似于 Python 的生成器,但有一个关键区别:Lua协程可以在嵌套调用中 yield,而在 Python 中,生成器只能从它的主函数中 yield。在实现上,这意味着每个协程像线程一样必须有独立堆栈。和“平坦”的生成器相比,“带堆栈”的协程发挥了不可思议的强大威力。例如,我们可以在它们的基础上实现一次性延续点 (one-shot continuations)。

7.2 经验

对于你做的这些,你如何定义成功?

Luiz:一种语言的成功,取决于使用该语言的程序员数量以及使用它的应用程序的成功。其实,到底有多少人在使用 Lua 编程,我们并没有确切的答案,不过毫无疑问的是,有很多成功使用 Lua 的应用程序,其中包括一些非常成功的游戏。同样地,使用 Lua 的应用程序的范围,从桌面图像处理到嵌入式机器人控制。这表明 Lua 具有一个非常明确的小众市场。最后,Lua 是唯一一种由发展中国家创建并在全球获得广泛应用的语言。它也是 ACM HOPL 唯一重点推介的语言。

Roberto:这很难定义。我曾经在多个领域工作过,在每个领域我从不同的方式在感受了成功。总之,我想说这些的共通之处在于:“被人知晓”。被认可,被公认,被人们推荐,这些都让人非常开心。

对于这门语言,你有什么遗憾吗?

Luiz:我确实没有任何遗憾。不过,事后回想起来,如果我们当初知道我们现在正在做的事情该怎么做的话,这些事情本可以早点完成!

Roberto:我不确信我有什么具体的遗憾,不过语言设计会牵涉到很多困难的决策。对我来说,最困难的决策是在易用性方面。Lua 的目标之一是让非专业程序员易于使用。我没有契合这种定位。因此,当我把自己当作用户,从这个视野来看,有关 Lua 语言的某些决策并非最佳。Lua 的语法就是一个典型的例子:虽然 Lua 的很多应用都得益于其冗长的语法,不过,就我自己的口味而言,我更偏爱紧凑的符号。

你在设计或实现时犯过错吗?

Luiz:我认为我们在设计或实现 Lua 时,并没有犯什么大错。我们学着如何发展一门语言。这绝不仅仅是定义它的语法和语义并将其实现。还有许多重要的社会问题,比如说创建并支持一个社区。这需要通过多种途径,编撰手册、写书、维护网站、邮件列表以及聊天室等。毫无疑问,我们认识到了支持一个社区的价值,明白了做这些工作需要极大的投入,并不亚于在设计和编码工作中的投入。

Roberto:我们很幸运,没有犯什么大错。我们在这个过程中还是出了许多小问题。作为 Lua 演化发展的一部分,我们有机会修正它们。当然,版本间的不兼容问题会让一些用户感到烦恼。好在 Lua 现在已经非常稳定了。

对于成为一名优秀的程序员,你有什么建议?

Luiz:永远不要害怕重新开始,这当然是说到容易做到难。永远不要低估需要注意的细节。你认为未来可能会用到的功能,就不要马上添加了:现在增加这个功能只会让你日后真的需要这个东西时,那些更好的特性很难加入。最后,永远追求更为简洁的解决方案。诚如爱因斯坦所言:尽量简洁,然过犹不及 ( As simple as possible, but not simpler. )。

Roberto:学习新的编程语言,不过一定要读好书!Haskell 是所有程序员都应该学会的一种语言。学习计算机科学:新算法、新形式体系(如果你还不了解,可以看一下 Lambda 演算,或是 pi 演算,CSP 等等)持续改进你的代码。

计算机科学的最大问题是什么?我们又如何教授呢?

Roberto:我想还没有什么能像“计算机科学”那样表达一种广为人知的知识集。并不是说计算机科学不是科学,而是说太难定义什么是计算机科学,什么不是(以及什么重要什么不重要)。计算机科学界的很多人都没有一个正规的计算机科学背景。

Luiz:我把自己当成是一名对计算机在数学中扮演什么角色感兴趣的数学家。当然,我非常喜欢计算机。:)

Roberto:即使是那些有正规计算机科学背景的人,也没有达成共识,我们缺乏一个交流的共同基础。很多人认为是 Java 创建了监视器、虚拟机以及接口(相对于类)等。

是不是有很多计算机科学学科仅仅只是一种职业训练?

Roberto:是的。而且,很多程序员甚至连计算机科学的学位都没有。

Luiz:我并不这么认为,但我不是作为一名程序员被雇用的。从另外一方面来说,我认为,要求程序员有计算机科学学位或是诸如此类的认证是错误的。计算机科学学位并不代表很好的编程能力。很多优秀的程序员也没有计算机科学学位(或许这只在我开始编程时成立;现在我可能是太老了)。我的观点是,一个人拥有计算机科学学位并不能保证他程序写得好。

Roberto:要求所有的专业人士都拥有学位是不对的。但我的意思是这个领域的“文化”太薄弱。几乎没什么东西需要人们必须知道。当然,雇主可以制定自己的要求,但不应该对学位有严格规定。

数学在计算机科学,特别是编程方面,起到一个什么作用?

Luiz:好吧,我是一位数学家。对我来说,数学无处不在。我之所以被编程所吸引,很可能是因为它具有数学的特性:精确、抽象和优雅。编写一个程序有如对一个复杂定理的证明,你可以持续不断地精炼和改进,而且它还能干点实际的事情!

当然,我在编程时根本没想这些,不过我认为,数学的学习对于编程是非常重要的。它有助于带你进入一种特定的心境当中。如果你习惯以抽象事物的自身法则去思考问题,编程就变得更简单。

Roberto:按照 Christos H. Papadimitriou 的说法,“计算机科学是新的数学”。一名程序员如果没有数学功底,就很难有大的作为。从更广的视野来看,数学和编程都具有一些共同的思想原则:抽象。它们都使用同一个关键工具:形式逻辑。优秀的程序员任何时候都在使用“数学”,利用它来确立 code invariants 以及接口模型等。

很多编程语言都是数学家创建的——或许这就是编程困难的原因所在!

Roberto:我会把这个问题留给我们的数学家。

Luiz:好的,此前我已经说过,编程绝对具有数学品质:精确、抽象、优雅。对我来说,设计编程语言就像是构建一种数学理论:你提供了功能强大的工具,其他人可以使用它来做很出色的工作。我一直被那些规模小而功能强的编程语言所吸引。强大的原语和结构之美如同强大的定义和基本理论之美。

你是如何区分出优秀的程序员的呢?

Luiz:你也知道。如今,糟糕的程序员更容易识别——不是因为他们的程序很糟糕(尽管那些程序通常非常复杂又混乱不堪),而是因为你可以感觉到,编程对他们来说并不愉悦,好像他们写的程序对他们自己来说是一个神秘事物,一种负担。

调试技能如何教授?

Luiz:我认为调试无法教授,至少不能正式地教授。不过当你跟别人,一个或许比你经验更丰富的人,一起调试的时候,你可以通过具体案例来学习。你可以从他们那里学习调试策略:如何去缩小问题范围,如何去做出预测和评估结果,判断哪些是没有用的,只是些噪音而已。

Roberto:调试本质上是在解决问题。它是一个需要来调动你已学会使用的一切工具的活动。当然存在一些实用的技巧(例如,如有可能,尽量不用调试器,在用 C 这样的底层语言编程时,使用内存检查器),不过,这些技巧只是调试的一小部分。必须像学习编程那样学习调试。

你如何测试和调试你的代码呢?

Luiz:我主要是一块一块的构建,分块测试。我很少使用调试器。即使用调试器,也只是调试 C 代码。我从不用调试器调试 Lua 代码。对于 Lua 来说,在适当的位置放几条打印语句通常就可以胜任了。

Roberto:我差不多也是这样。当我使用调试器时,通常只是用来查找代码在哪里崩溃了。对于 C 代码,有个像 Valgrind 或者 Purify 这样的工具是必要的。

源代码中的注释起到什么作用?

Roberto:用处不大。我通常认为,如果有什么需要注释的,那只是因为程序没写好。对于我来说,一条注释更像是打了个便签,它在说“以后记得重写这段代码”。我认为清晰的代码要比带注释的代码可读性更强。

Luiz:我同意。我一直坚持:注释应该用来表达代码不能清晰表达的东西。

一个项目应该如何文档化呢?

Roberto:强制执行。没有什么工具可以代替一份井井有条、深思熟虑的文档。

Luiz:但是,为一个项目的发展历程写出好的文档,唯一的可能就是从一开始就把这一点放在心上。Lua 并没有这样做;我们从来没想到 Lua 能发展这么快,并在今天获得这么广泛的应用。我们在撰写 HOPL 论文的日子里(这花了将近两年时间!),我们发现已经很难记起当时是怎么做出一些设计决策的了。从另外一个角度来说,如果早期我们要求会议都有正式的会议记录,可能就会失去一些自发性,并错失一些乐趣。

在代码库的发展历程中,你需要权衡哪些因素?

Luiz:我会说“实现的简单性”。这样做的话,速度和正确性随之而来。同时,灵活性也是重点,这样,如果需要,你可以换一个实现方式。

可用的硬件资源如何影响程序员的心态?

Luiz:我是个老家伙了。我是在一台 IBM 370 上学习的编程。要花上几个小时来给卡片穿孔、提交给队列再等到打印输出。我见过各种各样的慢机器。我认为程序员应该体验一下这些机器,因为并不是世界上人人都有最快的机器。编写给大众使用的应用程序的人应该在慢机子上试一下,这样才可以获得更广泛的用户体验。当然,仅可能用最好的机器来开发:把大量时间花在等待完成编译上可一点也不有趣。在现在的全球因特网中,Web 开发者应该尝试慢速连接,而不是他们工作机上的超快连接速度。以平均水平的平台为目标,会让你的产品速度更快、更简单,而且更好。

就Lua来说,“硬件”是指 C 编译器。我们在实现 Lua 的过程中学会的一点就是:以可移植性为目标确实值得。几乎从一开始,我们就是用非常严格的ANSI/ISO C (C89) 来实现 Lua 的。这样一来,Lua 就可以在专用硬件上运行,比如机器人、打印机固件和网络路由器等,这些没有一个是我们当初的实际目标平台。

Roberto:你应该始终认为硬件资源有限,这是一条金科玉律。它们当然总是有限的。“自然厌恶真空”;任何程序都有扩展的趋势,直到它用完了所有的可用资源。此外,随着确定平台上的资源越来越便宜的同时,又会出现一些有严格限制的新平台。微型计算机是这样;移动电话是这样;一切都是这样。如果你想做成市场第一,你最好要时刻关注你的程序需要什么资源。

对于现在或者不久的将来开发计算机系统的人,你在发明、开发和完成你的语言方面,有什么经验可以说的吗?

Luiz:我认为,程序员应该始终记住:并非所有的应用程序都是运行在功能强大的台式机或者笔记本电脑上的。很多应用程序要运行在受限的设备上,比如说手机,甚至是更小的设备等。设计和实现软件工具的人们应该特别关注这个问题,因为没有人会告诉你,你的工具会在什么地方如何使用。因此,就应该为使用最小的资源而设计。你可能会惊奇地发现:很多环境使用了你的工具,而你并没有把这些环境作为主要的应用目标,你甚至都不知道它们的存在。Lua 就碰到过这种事!而且这很自然;我们内部有一个笑话,这其实不是一个真正的笑话:我们讨论在 Lua 中的一个特性的细节时,我们问自己,“好的,不过它会不会在微波炉上运行呢?”

7.3 语言设计

Lua 易于嵌入,而且要求的资源也非常少。你是如何设计的,使得它适应硬件、内存和软件资源都很有限的情况?

Roberto:开始时,我们并没有把这些目标搞得很明确。我们只是为了完成项目才不得已而为之。随着我们的发展,这些目标对我们来说变得更为清晰。现在,我想各方面的主要问题都始终是经济问题。例如,无论什么时候,有人建议一些新的特性,第一个问题就是需要多大的成本。

你有没有因为特性成本太高而拒绝添加它们呢?

Roberto:几乎所有的特性,相对于它们能带给语言的东西来说,都“成本太高”。举一个例子,甚至一个简单的 continue 语句都不符合我们的标准。

添加一个特性需要带来多大的收益才是值得的呢?

Roberto:没有固定的规范,不过看该特性是否能让我们感到“惊喜”是条好的判断标准;也就是说,不仅仅满足其初始其初始动机。这让我想起了另一条经验法则:多少用户会从该特性中受益。某些特性只对一小部分用户是有用的,而其他特性对于几乎所有人都是有用的。

你有例子说明一条新特性对很多人都有用吗?

Roberto:for 循环。我们甚至反对过这个特性,不过当它出现时,它改变了书中所有的例子! 弱表也是出奇地有用。使用它们的人并不多,不过他们应该试试。

在 1.0 版本之后的多年里,你都没有把 for 循环加上。是什么驱使你不加它?而又是什么使你最终加入了它?

Roberto:我们曾无法找到一种让循环通用而简洁的格式,以至于我们一直不肯加入它。当我们发现可以使用一个生成器函数这样一个不错的形式后,我们就把 for 循环加上了。实际上,闭包是使生成器简单通用的要素。因为把生成器函数做成闭包,可以在循环过程中保留其内部状态。

更新代码来获取新特性的优势,重新得到更好的编程实践经验,这些会引起大块费用吗?

Roberto:新特性不是必须使用的。

那么人们会选择一个 Lua 的版本一直用到整个项目的生命期结束,从不升级吗?

Roberto:我认为,在游戏领域大多数人确实是这样做的。而在其他领域,我认为有一些项目不断更新他们所用的 Lua 版本。不过有个反例,魔兽世界从 Lua 5.0 更新到了 5.1 !请留意 Lua 现在要比早年的时候稳定多了。

你们在开发过程中是如何分工的,特别是在编写代码方面?

Luiz:Lua 第一版是由 Waldemar 在 1993 年编码的。自 1995 年左右以来,Roberto 编写和维护了主要代码。我负责一小部分:字节码 dump/undump 模块和独立编译器 luac 。我们一直在修改代码,并通过电子邮件向其他人发送代码修改建议,而且,我们就新特性及其实现开了很长时间的会议。

你从用户那里得到了很多有关语言和实现的反馈吗?对于在语言中加入用户反馈及其修改,你有一个正式的机制吗?

Roberto:我们开玩笑说:你要是忘了什么,那它肯定不重要。Lua 讨论列表非常活跃,不过一些人将开放软件和社区项目等同视之。有一次,我向 Lua 列表发送了以下消息,总结了我们的方法:

Lua 是一款开放软件,不过它从未进行过开放式开发。这并不意味着我们没有听取其他人的意见。实际上,我们几乎阅读了邮件列表中的每一条消息。Lua 里面的若干重要特性就起源或发展至外部的贡献(元表、协程,以及闭包的实现,这里仅举出几个重要的名字),不过,一切都要由我们来最终决定。我们这么做并非觉得我们的判断要比其他人的更好。而仅仅是因为我们想让 Lua 成为我们想要的语言,而不是世界上最流行的语言。

由于采用了这种开发风格,我们不愿意为 Lua 建一个公开的代码仓库。我们不想会我们做的每一处代码修改处处解释。不想为所有的更新保留文档。我们想在有些奇怪的想法时,有足够的自由来试一下,不满意的话就放弃掉,而不需要对每个行动都做一个解释。

为什么你喜欢获得建议和想法,而不是代码?我在想,或许你自己写代码能够让你学到关于问题(解决方案)的更多知识。

Roberto:差不多可以这么说。我们喜欢彻底搞清楚在 Lua 中发生了什么,因此,一段代码贡献不大。一段代码并不能解释为什么采用这种方式,但是,一旦我们理解了它的根本思想,编写代码就成了我们不想错过的乐事。

Luiz:我想对于引入第三方代码还有一个问题,我们无法确保其所有权。我们肯定不想溺死在要别人把代码授权给我们的合法化的过程中。

Lua 会不会达到这种状态:你已经添加了所有想要添加的特性,唯一需要的就是改进实现(例如,LuaJIT)?

Roberto: 我觉得现在就处于这种状态。我们已经添加的特性,即使不算是全部,也是我们想要添加的绝大部分。

你是如何操作冒烟测试和回归测试的?使用开放代码仓库的一大好处是,你可以让人们对几乎每一个修改进行自动测试。

Luiz:Lua 的发布并没有那么频繁,因此,发布一个版本时,已经进行过很多的测试。当这个版本已经相当可靠时我们才发布工作期版本 ( work version / pre-alpha 版),人们能够看中看到新添加的特性。

Roberto:我们确实进行了严格的回归测试。重点在于:因为我们的代码是用 ANSI C 编写的,基本上没有什么可移植性问题。我们没有必要在若干不同的机器上进行测试。一旦修改了代码,我就会执行所有的回归测试,不过这一切都是自动进行的。我要做的只是敲一下 test all 。

如果发现了一个反复出现的问题,到底是局部临时解决,还是全局通盘考虑,你如何判断哪一种是最佳解决方案?

Luiz:我们一直尽量做到一发现 bug 就修复它。不过,因为我们并不经常发布新的 Lua 版本。所以我们都是等到有足够的修复量才发布一个小版本。大版本做的都是改进工作而不是修复 bug 。 如果问题非常复杂(这种情况很罕见),我们会提供一个小版本作临时解决方案。而在下一个大版本中通盘考虑来解决它。

Roberto:通常,局部的权宜修复很快就可以完成。只有在确实不可能进行全局修复时,我们才会作局部的权宜方案。例如,如果某个全局修改需要一个新的不兼容接口。

从开始到现在,已经过去了这么多年,你仍然会为有限的资源而设计吗?

Roberto:当然会的,我们一直致力于此。我们甚至考虑过改变 C 结构内的字段顺序,以节省几个字节。:)

Luiz:相比于以前,现在有更多的人们把 Lua 语言运用到比以前更小的设备上面。

以用户视野来对简单性的追求怎样影响语言设计的?我想起了 Lua 对类的支持,让我想起了许多在 C 中实现面向对象的方式(不过没那么另人烦恼)。

Roberto:目前,我们有一个准则叫“机制而非法策”。它可以保证语言简洁,不过就像你说的,用户必须提供它自己的法则。就类这个问题来说,有很多方法实现它。有些用户会喜欢某种方式,而其他用户则可能痛恨它。

Luiz:这个确实赋予了 Lua 一种 DIY 的风格。

Tcl 也用了一种类似的方法,不过各家各有其法使它支离破碎。因为 Lua 有特定的目的,所以分裂对它不是啥严重问题吗?

Roberto: 对。有时这是个问题。但对于大量应用(比如说游戏)来说,这不是个问题。Lua 主要用来嵌入到别的应用程序中。而应用程序会提供一个坚固的框架来统一编程规范。你看到了 Lua/Lightroom, Lua/WoW, Lua/Wireshark —— 这个每个都有自己的内部文化。

你认为 Lua 这种“我们提供机制” 的展延性风格,给人带来巨大的好处吗?

Roberto:这么说并不确切。对于大多数事情来说,它是一种折衷处理。有时候,提供即刻可用的规范法则非常有用。“我们提供机制”更为灵活,但需要做更多的工作,并使得风格分裂。这最终也是个经济问题。

Luiz:另一方面,有时候这很难向用户解释。我的意思是,让他们理解是这些机制是什么,以及这些机制的原理。

这会使项目之间交流代码变得困难吗?

Roberto:没错,通常就是这样。它也阻碍了独立库的发展。例如,WoW 拥有大量的库(甚至连用遗传算法解决货郎担问题的库都有),不过在 WoW 之外却没人去用它们。

你担心 Lua 会因此分裂成 WoW/Lua,Lightroom/Lua 等分支吗?

Luiz:我们并不担心:语言还保持相同,只是可用的函数不同而已。我认为这些应用程序会在某些方面受益于此。

严肃的 Lua 用户会在 Lua 基础上编写他们自己的方言吗?

Roberto:很有可能。至少我们还没有宏。要是有宏的话,我认为你可以使用宏来创建一种真正的方言。

Luiz: 本质上还不算一种语言的方言。不过算是用函数来实现的一种特定领域语言。这曾是 Lua 的设计目的之一。当 Lua 仅仅用来作数据文件时,它看起来是一种方言,当然那些只是 Lua 表而已。有些项目或多或少实现了一些宏。比如我想起了 metalua 。这也是 Lisp 的一个问题。

你为何选择提供一种可扩充的语义?

Roberto:它开始是作为提供面向对象特性的一个方法。我们不想在 Lua 中添加 OO 机制, 但用户想要这些。我们想到这个方法,提供足够的机制让用户实现自己的 OO 机制。到现在我们也觉得这是一个正确的决策。然而,这使得用 Lua 的方式 OO 编程对于初学者来说更为困难。但它也给语言带来了大量的灵活度。特别是,当我们把 Lua 和其它语言混用(这是 Lua 的一个特色)时,这种灵活度使得程序员可以让 Lua 的对象模型去适应外部语言的对象模型。

目前的硬件、软件、服务和网络环境同你最初设计时的系统环境有何不同?这些变化对你的系统以及未来的改变有何影响?

Roberto:因为 Lua 是以极高的可移植性为目标,我认为目前的“环境”同以前的环境并没有什么不同。例如,我们开始开发 Lua 时,DOS/Windows 3 跑在 16 位机器上;一些老机器仍然是 8 位的。目前我们没有 16 位的台式机了,不过,若干使用 Lua 的平台(嵌入式系统)仍然是 16 位或者甚至是8位的。

最大的变化在于 C 语言。回头看 1993 年,当时我们刚开始做 Lua ,ISO (ANSI) C 还没有像今天这么成熟。很多平台仍然使用 K&R C 。很多应用程序写了一些很复杂的宏来使得程序通过 K&R C 和 ANSI C 两者的编译。主要的区别在函数头的声明。当时,坚持使用 ANSI C 是一个冒险的决定。

Luiz:我们仍未感觉到有必要转移到 C99 上面。Lua 是用 C89 实现的。如果过渡到 64 位机器上时出现些小毛病的话,或许我们必须使用 C99 的一部分(特别跟长度有关的类型定义),不过我并不希望出现任何问题。

如果能全部重新构建 Lua 的 VM 的话,你仍然会坚持使用 ANSI C 吗,或者你希望有一个更好的语言用于跨平台的底层开发?

Roberto:不。ANSI C 是我(目前)知道的可移植性最好的语言。

Luiz:有些杰出的ANSI C编译器,不过,即使是使用它们的扩展,也不会给我们带来很多性能提升。

Roberto:改进 ANSI C 并保持它的可移植性和性能并不容易。

顺便问一句,你是说 C89/90 吗?

Roberto:是的。C99 尚未确认好。

Luiz:再者,我不确定 C99 能给我们带来很多额外的特性。我还特别想到了 gcc 中使用的带标签的 goto 语句作为 switch 的一种替代方案(在虚拟机执行的主干里)。

Roberto:在很多机器中,这样做可以改进性能。

Luiz:我们早期对它作过测试,最近也有人也对它进行了测试,效果并不吸引人。

Roberto:部分原因在于我们基于寄存器的体系结构。它倾向于用较少的操作码,每个操作码分担更多的工作。这减少了分发器的负担。

你为什么要构建一个基于寄存器的 VM 呢?

Roberto:为了避免所有的 getlocal/setlocal 指令。我们也想去实践一下我们的想法。我们想啊,如果它运行得不好,至少我们还能写一些研究这个的论文。而最后,它运行得非常好,而我们也只写了一篇论文。:D

在 VM 上运行对调试有没有帮助?

Roberto:它没有提供“帮助”;它改变了整个调试的概念。既调试过编译型语言,又调试过解释型语言(比如 C 和 Java)的人都知道它们天差地别。好的VM 会让语言变得更安全,在某种意义上,该错误可以从语言层面上理解,而非机器层面(比如说段错误)。

如果语言是平台无关的,这对调试有何影响?

Roberto:通常它有利于调试,因为一种语言越是和平台无关,它就越需要可靠的抽象描述和行为。

考虑到我们是人,而人总会犯错。你是否曾经考虑过:为了在调试阶段有所帮助,需要向语言添加某种特性或是从中删除一些特性?

Roberto:当然了。辅助调试的第一步就是良好的错误消息。

Luiz:从初期版本开始,Lua 中的错误消息就在一直改进。我们已经从可怕的“调用操作对象不是一个函数”的错误消息(这条错误消息一直用到 Lua 3.2),变成了更好的错误消息:“试图调用全局 'f' (一个 nil 值)”。从 Lua 5.0 开始,我们使用对字节码的符号追踪 (Symbolic execution) 来试着提供更有用的错误消息。

Roberto:在语言自身的设计中,我们一直设法避免使用复杂的结构。如果它很难理解,就会更难调试。

在设计一门语言和设计用这种语言编写的程序之间,有什么联系?

Roberto:至少对我来说,设计一门语言的要点在于从用户的角度出发,也就是说,去考虑用户将怎样使用每一个特性,用户将会如何将这些特性和其它语言对比。程序员总会找到使用一种语言的新方式,优秀的语言应该允许那些意想不到的使用方法。不过,语言的“正常”用法应该遵从语言设计者的初衷。

语言的实现会在多大程度上影响语言的设计?

Roberto:这是一条双向道。实现会对语言产生巨大的影响:我们不应该设计无法高效实现的东西。一些人忘了这点。在设计任何软件时,效率一直是一个(或者是惟一的)主要约束条件。不过,设计也可能会对实现产生较大的影响。一眼看去,Lua 的几个特色之处都来自于它的实现(体积小、优秀的 C API ,以及可移植性),而 Lua 的设计在使这些实现变得可能中,起到了关键作用。

我读过你的一篇论文,《 Lua uses a handwritten scanner and a handwritten recursive descent parser( Lua 使用一个手写扫描程序和一个手写的递归下降分析器)》。你是如何开始考虑手工构建一个分析器的?是不是从一开始就很清楚,这样做要比 yacc 生成的分析器要好?

Roberto:Lua 第一版使用了 lex 和 yacc 。不过,Lua 最初的主要目标之一是作为一种数据描述语言,和 XML 没什么不同。

Luiz:但是时间要更早一些。

Roberto:很快人们开始把 Lua 用于数兆字节的数据文件,此时 lex 生成的扫描器迅速变成了瓶颈。手写一个优秀的扫描器非常容易。而且只做了这么一点简单的改进后,我们就提高了 Lua 大约 30% 的性能。

决定从 yacc 改成手工编写解析器是很后来的事情,这个决定做得并不容易。这起源于几乎所有 yacc/bison 实现使用的主干代码的问题。

当时,它们的可移植性很差(例如,用了好多处的 malloc.h ,这是一个非 ANSI C 的头文件),而且,我们无法控制其整体质量(例如,控制堆栈溢出和内存分配错误等问题),而且它们也不是可重入的(比如要在解析代码的过程中调用解析器)。另一方面,如果你想要像 Lua 那样及时生成代码,自底向上解析器也不如自顶向下的那么好。因为它难以处理“继承属性(Inherited attributes)”。我们改写之后,发现我们手写的解析器要比 yacc 生成的那个略小以及略快一点。不过这不是改写的主要原因。

Luiz:自顶向下分析器还能提供更好的错误消息。

Roberto:不过,我从不推荐为没有成熟语法的语言手写解析器。并可以肯定LR(1)(或是 LALR 甚至 SRL)会比 LL(1) 强大多了。甚至对于 Lua 这样的简单语法的语言来说,我们也必须使用一些技巧来构建一个像样的分析器。例如,处理二元表达式的程序并没有按原始语法去处理,而是用了一个聪明的基于优先级(priority-based)的递归方案。在我的编译器课上一直向我的学生推荐 yacc 。

你的教学生涯中有什么趣闻轶事吗?

Roberto:我刚开始教授编程时,供我们的学生使用的计算机设备是一台大型机。有一次,一个非常优秀的团队提交的一个程序作业,居然连编译都没通过。我找他们来谈话,他们发誓用好几个测试案例仔细的测试了程序。当然了,他们和我用的是同一台机器,完全相同的环境,都是在那台大型机上。这个神秘事件只到几周后才搞明白。原来机器上的 Pascal 编译器被升级了。升级刚好发生在学生完成任务和我开始批改作业之间。他们的程序有一个很小的词法错误(如果记得没错,是多了个分号),而老的编译器没有检测到!