PostScript语言里的珠玑,PostScript的领域对象和操作

更新时间:2014-06-19 10:36:54点击次数:1070次

摘要:首位图灵奖得主Alan Perlis曾说:“如果一门编程语言不能影响你的思维,就没学习的必要。”能通过这个严苛测试的语言寥寥无几,但PostScript在测试中至少得A。它彻底地改变了桌面出版行业,许多特性至今仍值得借鉴。

PostScript的领域对象和操作

作为针对桌面出版的文档描述语言,PostScript的设计者力图要解决的核心问题,是如何设计一个灵活高效的语言,以操控桌面出版中各种各样的图形对象,并保证设备无关性。我们不妨戴上语言设计者的眼镜,来模拟一下这个过程。

我们面临的首要问题是如何描述桌面出版中的种种复杂对象和操作。尽管任何平面出版物最终都是二维像素点的集合,但我们并不希望这个语言局限于描述像素点的颜色。这个语言最好还能直接描述文字、线条、形状等设计师熟悉的对象。因为从根本上讲,如果我们要设计的描述语言没有足够的表达能力,不能精简高效地表达图片、字体、形状、颜色等桌面出版领域的业务对象,这个语言将不可避免地“难用”。一般来说,把领域特定语言设计得“好用”,需要深厚的领域知识(domain knowledge)。所幸的是,PostScript的设计者们,原先在施乐PARC从事激光打印机控制语言设计,对于桌面出版可算驾轻就熟。因此,他们毫不费力地选取了Bézier曲线、矢量字体、绘图路径(Path)等作为整个绘图系统的基本结构。在对这些对象的操作上,PostScript选取了平移、旋转、放缩等仿射变换,加上路径操作和字体控制,构成了一个强大但规整的绘图系统。

PostScript绘图系统的设计深刻影响了后来的许多矢量图形系统。举例说,如今计算机使用的矢量字体均采用Bézier曲线描述,即起源于PostScript;如今几乎所有的矢量绘图语言都支持的“路径”,也起源于PostScript。我们不在此详细展开这些领域对象选取背后的原因,对PostScript感兴趣的读者可以阅读《PostScript Language Tutorial & Cookbook》(也称“Bluebook”)了解PostScript的一些基本概念。

PostScript的语言设计

基本领域对象确定后,接下来就是力求设计出一个“灵活高效”和“设备无关”的语言来控制这些领域对象。设计目标落实为具体需求,包含以下三点。

第一,语言本身要能表达曲线、字体、图片、形状等领域对象;颜色、分页及这些对象的平移旋转等操作,在语言里最好也都是一等公民,能直接表达。

第二,语言的表达能力要足够强大,最好是图灵完全,以支持灵活的需求。

第三,语言要与设备无关,也就是说,语言将运行在一个虚拟机或解释器上,而非直接编译为二进制代码。

考虑到我们要设计的语言是针对桌面出版的,最终还要加上一条:这个语言的语法和结构要足够简单,使得非编程专业人士也能使用。

有了需求的指导,我们不难理解PostScript所采取的设计:以一个易用的、图灵完全的语言作为蓝本,加入众多针对桌面出版的对象操作,并实现一个轻量的、与设备无关的解释器。事实上,PostScript是以FORTH语言作为蓝本设计的。选取FORTH的主要原因,是因为它是一个轻量级的、基于栈虚拟机的语言。FORTH的表达能力和易用性当时已被实践所证明,因此借用它的基本控制语法就是一个很自然的选择。

逆波兰表示法和度量单位

逆波兰表示法是FORTH和PostScript等基于栈的语言的一个鲜明特点。在ALGOL家族语言中,3乘以4的一般写法是3 * 4,即运算符中缀。PostScript将运算符后缀,写作“3 4 mul”。意思是将3、4分别推入栈中,然后将乘法(multiply)操作运用于两个栈顶元素(弹出),并将乘积结果入栈。FORTH仍然采用+、*等数学符号。PostScript规范化了所有的操作符,一致采用add、mul等单词操作符来代替+、* 等传统的中缀操作符。我们稍后将阐明规整化的优点。这里只需要了解一点:PostScript程序本质上是一个后缀表达式。PostScript没有所谓的语法,只有栈操作。如果非要说有语法,那就是逆波兰表示法。这一点非常类似于LISP——所谓的语法,就是S表达式。

PostScript允许以闭包定义新操作符,其中,闭包是放在{}中的后缀表达式。例如,“乘以3”这个操作可定义为:/mul3 { 3 mul } def。这里,/mul3表示取“mul3”的符号值。{ 3 mul }是一个闭包,而def将mul3这个符号,映射到{ 3 mul }闭包。据此,4 mul3即为4 3 mul。

其实,从语法上看,/mul3 { 3 mul } def和3 4 mul并没有明显的不同:都是前两个操作元入栈,最后一个操作符进行运算。也就是说,PostScript的栈是异构的,符号、数字和闭包都可以放入栈中。许多操作符如if,也依赖于栈上有一个布尔值和一个闭包。这种不在栈中区分代码和数据的设计,允许我们重写栈上的闭包。实际上我们可以证明这个特性等价于LISP里的宏(Macro)的表达能力,限于篇幅,我们不在这里展开。

现在,我们从mul3这个平淡无奇的例子出发,定义一个英寸(inch)的操作符:/inch {72 mul} def。一眼看去,{72 mul}是闭包,而inch是长度单位,两者毫不相干,为何强拉在一起?原来,PostScript的基本长度单位是1/72英寸,因此5 inch即展开为5 72 mul,或者说360个基本单位。Inch的定义使得我们可以书写1.2 inch 2.3 inch moveto这样直观的程序。

用闭包定义常用度量单位在PostScript中并不少见。对于从未接触过这种定义方法的读者来说,相信inch这个例子让人印象深刻,因为它昭示了度量单位的实质:度量单位是后缀闭包。比如我们说10美元时,已在自觉或不自觉地将“美元”单位替换成 {汇率 mul}闭包,换算成60元人民币等。实际上,任何度量单位之所以能被我们感知,都是因为我们脑中的一个潜在后缀闭包的作用。在摄氏度体系下的人对华式温度没有感觉,或者仅接触一定数量级范围内的人对大数字不敏感,都是由于一个原因:我们尚未建立一个将不熟悉的单位或数量级转化为可感知的单位或数量级的闭包。

PostScript的运行时字典栈

除基本控制语法外,PostScript引入了对于图形处理很重要的两个基本数据结构:字典和数组。可以想象,存有一系列点的数组可以表达一个字符的轮廓,而字典可以很好地表达一套字体。不仅如此,通过字典栈这个概念,PostScript具有了FORTH和其他栈语言所完全不具有的动态特性。我们仍然以一个例子说明。

我们定义一个求直角三角形斜边长度的操作hyp,即/hyp { dup mul exch dup mul add sqrt } def(这里dup表示重复栈顶元素,exch表示交换栈顶两元素,sqrt为平方根,读者可以自行验证这个函数的正确性)。 这里,3 4 hyp得到5。

对解释器来说,我们新定义的hyp与mul并没有本质的不同(后缀表达式和规则化带来的便利)。解释器处理这些操作符时,无论是语言预先定义还是用户定义的,不可避免地需要进行符号表查找。可能的区别仅是到不同的符号表里查找。进一步说,一个叫inch的符号在没有进行符号表查找之前,我们根本不能确定这究竟是一个变量,还是一个闭包。

为了一致地处理符号表的查找操作,PostScript引入了字典栈(dictionary stack)的概念。字典栈是一个由解释器维护的栈,而栈中的元素则是作为符号表的字典。解释器启动后,系统字典systemdict中含有所有预定义操作符和变量,如add、mul等。用户字典userdict将涵盖自定义的操作符和变量。用户也可以随时建立新的字典插入字典栈中。

以字典方式存储符号表是容易理解的,可是为什么需要把这些字典加入“栈”中呢?原来,PostScript是按栈的顺序在字典中寻找操作符的。假如定义“/mul {add round} def”,则当前字典中的mul会被优先使用,而系统定义的mul不再可见。乍看之下,这和面向对象语言里提到的运算符重载概念类似。实质上,PostScript的设计要灵活许多。

首先,因为字典栈的存在,每个运算符都自动有了作用域(预定义的运算符因为存在于systemdict中,从而有全局作用域)。通过字典栈,我们可以实现其他语言中的lambda表达式或者Java中的匿名内部类。PostScript的运算符本质上是动态作用域的,但因为字典栈的存在,我们可以轻松实现词法作用域,方法即是在作用域中临时定义一个字典,在字典中定义新的操作符,并将字典推入字典栈。这样,只要在作用域结束时弹出临时字典,操作符定义也随之撤销。许多PostScript程序都采用这种方法构建。

其次,字典栈巧妙地支持了局部变量。和闭包一样,局部变量的本质是有作用域的值。基于栈的语言对函数局部变量是不友好的,因为局部变量本身是对处理器寄存器的抽象,访问局部变量也是采取随机存取而非按栈顺序存取的方式。而栈机器本身不直接支持寄存器抽象。熟悉JVM的读者都知道,JVM的{a,i,l,f,d}{load,store}系列指令,非常繁冗地支持局部变量数组和栈之间的转存。在字典栈中,局部变量有了优雅的解决方法:通过建立临时字典,我们可在不引入复杂的转存操作下,随机存取随机变量,而且局部变量的作用域得到了保障。比如,以下程序定义了一个叫做local_variable的局部变量,作用域仅限于/sample_proc。而将something换成{something}闭包,即是一个局部的操作符定义。

/sample_proc

 { 1 dict begin % 定义一个大小为1的临时字典

/local_variable something def

    end   % begin end 之间为字典元素

    …   % 具体的函数定义

 } def

PostScript和语言的Annotation

因为.ps文件本质是一个程序而非文档,打印PostScript文件的过程实质上是调用PostScript解释器执行程序的过程。因为PostScript的图灵完全性,在PostScript程序执行完之前,我们对文档的结构信息,例如一共多少页,文档有没有彩色元素等结构化的信息一无所知。PostScript设计于桌面出版业尚未起步之时,因此仅关心绘制控制,并未考虑到如何表示这些结构信息,这样的缺憾是可以理解的。HTML语言也经过了这样的道路:早期引入FONT BIG这种纯展示标签,而如今最佳实践是将结构信息放入HTML,而将格式信息交给CSS。

因为PostScript的成功,越来越多的人希望作为桌面出版标准格式的PostScript能包含文档结构信息。比方说,如果打印管理系统能在将PostScript任务交给打印机之前知道文档的页数,就可以更好地调度打印任务,或按页面收取费用等。这些关于文档的结构信息并不影响页面的展示,却是文档不可或缺的一部分。


为解决这个问题,PostScript用户自发地定义了一种通过注释表示文档结构信息的方法。例如,在一个10页的文档开头加入%%Pages: 10,每一页的开始加入%% Page N等。因为是注释,PostScript解释器可以选择忽略它,而其他程序则可以据此管理文档。许多桌面出版软件也采取这样的方法写入作者、创建日期等信息。在强大的需求和既定行业标准的驱动下,Adobe终于决定标准化这些用来表征文档结构的注释,发布了一系列的“文档结构约定(Document Structuring Conventions)”。之所以叫约定,是因为木已成舟,无法强行要求每个PS文档管理器或打印机都遵守标准。

DSC使得静态结构检查变得可能。前文提到,PostScript语法就一种后缀表达式,静态语法检查并没有意义,而正确性检查却又非常难。引入文档结构约定后,我们就有条件检查一些约束,比如在宣称的描述一页的区块之内没有非法的分页操作等。DSC不影响现有语言逻辑,却引入了新的语义正确性约束。

DSC这种引入新的元信息以静态检查程序的语义正确性的思想非常有前瞻性。可惜的是,因为了解PostScript的人较少,这样的思想没能在其他语言中实现。Java 5.0才正式引入了annotation的概念,用@override这样的标记帮助编译器检查方法多态。Python 2.2引入classmethod、instancemethod等decorator以检查方法的定义,而C++最近才正式支持annotation。这些比程序本身更抽象的元信息,越来越多地成为了自动分析工具的帮手。在Google,我们采用一套线程安全的标记以帮助编译器静态检查代码的线程安全性。所有这些都成提升开发效率的好帮手。而PostScript,是我所知的第一个以元信息约束程序语义的编程语言。

其他一些有趣的历史

PostScript语言的历史很有趣也很能给人启发,限于篇幅我仅录几则。首先,PostScript其实和Smalltalk很相似。因为同样出自于施乐PARC的研究,PostScript语言风格受Smalltalk影响很大。比如闭包的设计,if和repeat语法的设计,几乎就是Smalltalk的翻版,仅在运算符顺序上有区别。

Adobe的几位创始人从PARC独立出来后,最初力图开发一套打印机控制语言。熟悉这几位创始人的Steve Jobs认为,这个语言最重要的任务不是控制打印机,而是制作高品质文档。在Jobs的推动下,Adobe才开发了这套可以支持苹果当时正在开发的LaserWriter激光打印机产出高品质文档的语言——PostScript。从此,Adobe这家毫不起眼的小公司一举成为桌面出版革命最大的受益者。

因为PostScript语言灵活复杂,解释PostScript语言需要强大的微处理器。为此,Apple LaserWriter携带了一颗12MHz Motorola 68000处理器。而同时期与之相连的Machintosh计算机携带的却是一颗8MHz Motorola 68000处理器。

打印机处理器比主机更强大,用现在的眼光看真是不可思议的。桌面出版的革命来得如此之快,需要的计算能力如此之大,是整个个人计算机行业所没有预见的。或许,未来的3D打印技术或量子传输技术(Star Trek Transporter),会让这种情况重新出现。


  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息