语言设计的艺术——读《松本行弘的程序世界》

阅读数:6079 2011 年 11 月 18 日 00:00

利用 QCon 杭州 2011 大会的间歇期,读完了《松本行弘的程序世界》的最后几章,合上书还觉得意犹未尽。众所周知,松本行弘是 Ruby 的发明者,这本书是他的技术文集,主要章节在过去几年先后发表在日本的技术杂志上。坦白的说,我对 Ruby 语言本身没有深入的研究和实践,所以阅读本书的目的是想从一个旁观者的角度了解编程语言在设计方面的各种考量、各种语言的特性对比以及对编程开发的影响。毫无疑问,这本书满足了我的需求。不想说太多空泛的话,在这里分享一下自己的阅读心得和一些思考。

经常在技术论坛中看到类似于“xx 语言和 yy 语言哪一个更好?”“zz 语言有没有前途?”的提问,然后众说纷纭,群情激昂。有一种观点认为:编程语言都一样,学会了其中一种,另外的都触类旁通。这种说法有一定道理,各种语言在不少方面都存在共性,毕竟都是“语言”。不过,语言之间的差距也非常大,这也是编程语言层出不穷的原因。大部分编程语言都是“图灵完备”的,这意味着彼此可以实现等价的程序。不过,语言的选择在一定程度上决定着开发效率。由于语言适用的领域各不相同,而且语言的生态系统(依附的厂商、集成开发环境、虚拟机、社区推广、第三方函数库支持等)也许比语言本身更有影响力,所以我们只是从一般的角度来分析语言在设计上的考量。

面向对象

面向对象的设计方法已经深入人心,大多数现代语言都支持面向对象编程。多态性、数据抽象和继承是面向对象编程的三个基本原则。从数据抽象角度来说,Ruby 直接提供了栈等数据结构的支持,并隐藏了实现的细节。多态性和继承密不可分。目前在编程语言中存在多种继承方法,有多重继承、单一继承和 Mix-in 继承。对于开发人员来说,哪种方式更有效率呢?单一继承(如 Smalltalk)的优点是:继承关系是单纯的树结构,类之间的关系不会发生混乱,实现起来也较简单,缺点是:无法通过继承来共享多重程序代码,导致代码的冗余。多重继承(如 C++)的优点是:可以继承多个类的功能,扩展了单一继承。缺点是:类之间的关系会变得复杂,一个类可能有多个父类,这些父类又有自己的父类,继承关系不如单一继承清晰,继承的优先顺序和功能可能存在冲突。开发人员既想利用多重继承的优 点,又想避免它带来的麻烦,所以需要引入受限制的多重继承。Java 提供的解决方案是——接口。Java 只允许开发人员继承(extends)单个父类, 但是可以实现(implements)多个接口。仔细想想,Java 提供的这种解决方案是实现了“类规范”(即接口的方法声明)的多重继承,可以满足多态性的要求。但是,如果开发人员需要复用类的实现代码呢?如何完成“类实现”(类的实现代码)的多重继承呢?Ruby 的设计者松本行弘在评估了各种语言在这方面的优劣之后,借鉴了 Lisp 语言的 Mix-in 继承模式。这种模式的规则是:通常的继承用单一继承实现;第二个以及两个以上的父类必须是 Mix-in 类。Mix-in 类的特征是:不能单独生成实例;不能继承普通类。这种继承模式可以保证类的层次结构和单一继承一样的树结构,同时又可以实现功能共享。开发人员可以把想要共享的代码放在 Mix-in 类中,然后把 Mix-in 类插入到继承结构中,从而满足“类实现”的多重继承。开发人员利用支持 Mix-in 的语言做面向对象编程时会更加方便。值得注意的是,松本行弘在设计 Ruby 的时候,Mix-in 模式并不流行,但是他坚持了自己的判断,在 Ruby 中采用了该模式,时至今日,Mix-in 受到越来越多开发人员的欢迎。松本行弘的前瞻性建立在对各种语言深入的分析和评估的基础之上,具有扎实的依据,我们开发人员在预研某些技术时,不妨借鉴其做法。

元编程

元编程支持在编程语言特性中占有重要的地位,开发人员可能对反射等概念比较了解,“在代码中动态分析、生成代码”的元编程能力对于基于编程语言的开发框架来说很重要,如果语言自身提供了强大的元编程支持,框架的开发者会事半功倍。Ruby 提供了 attr_accessor 方法,支持开发人员动态生成访问变量的方法。Ruby 的反射功能可以获取、更改各种范围内的变量值,而且能够获取、删除类方法,以及其他一些分析功能,当开发人员希望实现“通用编程”的模式或者后面提到的猴子补丁时,这些元编程功能会提供有效的支持。相比之下,C、C++ 语言可能实现起来就比较困难,虽然存在宏定义等低效的办法。

高阶函数

高阶函数的使用同样可以提高开发效率,在函数模板化、容器迭代器等方面有着重要的应用。高阶函数在 C 语言中采用了传递函数指针的形式来实现,但是存在局限性,即实现函数间的信息传递只有两种方法,要么明确地传递参数,要么使用全局变量。这种限制导致代码编写的低效。为了解决此问题,Ruby 和 Javascript 语言引入了闭包的概念,即函数(块)可以引用外部的局部变量。通常的外部变量在方法执行结束时就不存在了,但是如果被包括进了闭包,那么在闭包存在期间,外部局部变量也会一直存在(当然,闭包也会引起潜在的内存泄露问题)。Ruby 中的块结构是高阶函数的一种特殊形式,代码块可以作为参数传递给方法,在被调用的方法中可以执行传递过来的代码块,执行后程序的控制权返还给方法,块中最后执行的表达式的值是块的值,这个值可以返回给方法。块结构的经典应用是对集合对象(容器)的处理,比如循环执行、条件排序、条件搜索等,开发人员只需把块结构传递给容器方法,就可以方便的执行块结构中的表达式并返回结果。之前 C++ 和 Java 等容器类的迭代器,使用别的类对象来处理容器元素,属于外部迭代器。Ruby 通过块结构和闭包实现了内部迭代器,不用额外生成对象。Ruby 中的集合方法非常丰富,包括 all、any、find、map、min、max、select、sort、inject 等,这样的设计能够让对数据结构和算法有要求的开发人员操作起来更加简洁和高效。

设计模式

提到设计模式,大家可能首先想到了著名的“四人帮”,还有他们归纳的 23 个设计模式。设计模式在软件开发中的作用不言而喻,开发人员会有意无意的借鉴这些模式,语言的支持程度对开发效率的影响不可小觑。Ruby 的语言库很丰富,提供了多种设计模式的支持。比如 singleton 库支持 singleten 模式、delegate 库支持 proxy 模式、块结构支持 iterator 模式、clone 方法支持 prototype 模式、observer 库支持 observer 模式,当然语言库的设计也利用了不少设计模式,比如上面讲到的容器集合方法 Enumerable 模块使用了 Template Method 模式来支持开发人员通过块结构来指定所需的算法。语言的设计者一方面要利用设计模式来优化语言库的结构,另一方面也会通过语言库自身来帮助开发人员在编程实践时更方便地使用设计模式。

猴子补丁

编程语言对于猴子补丁的支持对软件开发同样重要。猴子补丁可以解释为,不改变源代码而对功能进行追加和变更。软件开发过程中,有一个著名的开放 - 封闭原则(open-closed principle):对模块扩展必须开放,对修改必须封闭。模块是可以扩展的,比如追加新的数据结构或者功能,能够满足未来的需求。修改是封闭的,指被引用的模块内部细节发生变化时,对外接口应当是稳定的。猴子补丁能够遵循该原则,它的主要目的包括追加和变更功能、修补程序错误等。Ruby 这样的语言提供了开放类,也就是说类定义之后也能任意的追加新内容,不仅如此,Ruby 还提供了若干类操作方法,undef 可以取消之前本类或者父类定义的方法,alias 可以给方法起一个别名,开发人员可以在重新定义的方法中用别名来调用原来的方法,从而给原来的方法增加新功能,include 可以把其他模块的功能包含进来。Ruby 提供的这些方法使猴子补丁的实现过程更容易,对比 Java 等静态语言,读者可以发现 Ruby 语言在这方面处理灵活,开发效率更高。

数据类型与文字编码

语言的类型定义和编码是开发人员接触的基本知识,在日常工作中应用广泛。因此,语言在设计时对数据类型的内部实现是否具有扩展性和前瞻性就非常重要。比如语言选择的文字编码,有 UCS(Universal Character Set)方式和 CSI(Character Set Independent)方式。UCS 方式指输入输出时,语言把文本数据变成统一的文字集(如 UTF-8),内部对文本数据进行统一处理。UCS 的方式得到各种编程语言的青睐。而 CSI 方式则指不对各种文字集和编码方式做任何变换,原封不动的进行处理。UCS 和 CSI 方式的优缺点都很明显,这里不过多讨论。举例来说,Java 采用 UCS 方式,内部字符编码为 UTF-16,所以 Java 的 char 类型是 16 位。Java 语言诞生时,Unicode 仅限于 16 位,可以猜想这也是其设计者选择 UTF-16 编码方式的原因之一。但时过境迁,如今 Unicode 标准采用 21 位表示一个文字,所以 Java 的 API 需要升级才能处理变化之后的 Unicode 字符,开发人员可能需要作出相应的变更。Ruby 采用 CSI 方式,提供了若干编码方式的支持。我们不能笼统的说孰优孰劣,但是语言对字符类型和函数的设计对开发人员的效率有着直接的影响。同样的,整数、浮点数的类型定义也是语言设计的考察点。相比某些语言对数字类型的位数限制,Ruby 则提供了支持扩展的整数类型 Bignum、浮点数类型 BigDecimal,还有能够表示分数的 Rational 类。

函数式编程

函数式编程是与面向对象编程相提并论的编程方法,最近越来越受到关注,它的最大优点在于,程序可以按照数学的形式以及声明的形式来编写。支持函数式编程的语言能够帮助开发人员把工作重点放在描述算法上,而不是具体的实现操作。像 Lisp、Erlang 和 Ruby 都支持函数式编程,不少语言是各种结构化编程、面向对象编程和函数式编程的混合体,开发人员可以根据需要选择高效的编程方式。说起这个话题,笔者不禁想起技术专家老赵,他经常会在讲座前拿容器的集合方法为例对比 Java 和 C#的代码实现,强调声明式编程和 Lambda 表达式的好处,Ruby 这样的语言在设计时对此有所考虑,并选择了有益的实现。

虚拟机和垃圾回收

虚拟机的诞生从某种程度上解决了语言在软件开发中的跨平台问题,虚拟机对开发人员隐藏了操作系统级别的差异,语言库的 API 接口保持一致。除了少数传统语言,许多现代语言都使用了自动垃圾回收机制。开发人员无需手动处理对象的内存,省去了不少精力。当然,自动垃圾回收并不意味着没有内存泄露,错误的代码会让垃圾回收器无法释放废弃的对象。除此之外,垃圾回收器的性能也值得关注,如今垃圾回收算法多种多样,适用于不用的业务场景,开发人员需要关注。语言所依赖的虚拟机其可靠性和性能如何,是考量的一个因素,Node.js 的创始人就是因为对 Ruby 虚拟机的性能不满意而选择了 C 和 Javascript 作为 Node.js 的实现语言。

动态类型与静态类型

静态类型和动态类型之争一直在持续。静态类型的优点在于编译时能够发现类型不匹配的错误,方便做优化,提高程序执行速度。缺点是作为辅助信息的数据类型一定程度上影响了开发人员对程序本质的关注,而且不够灵活。动态类型的优点在于源代码变得很简洁,可以灵活的处理未指定类型的变量,包括只关心行为的 Duck Typing。缺点是在多数情况下,运行速度逊于静态类型语言,而且不执行程序就难以检测出错误。Java 是静态语言,Ruby 是动态语言,它们各有千秋,都有广泛的应用,但是目前看来,动态语言简洁的编程风格受到越来越多开发者的欢迎。谷歌最近推出的 Web 编程语言 Dart 则是兼顾了两者,开发人员可以根据自己的偏好和项目的阶段来选择是否为变量指定静态数据类型,按照其说法,项目初期采用动态类型快速构建,后期通过静态类型使程序更稳定和模块化,为开发人员提供了一种新思路。

小结

有关语言设计的讨论涉及到很多方面,我们无法一一分析。比如,语言对正则表达式的支持程度如何?异常处理的设计是否合理?数据持久化是否方便?并行处理的能力如何?这些方面在分析语言的优缺点时也需要谨慎的考虑。同时,语言的生态系统也是考量的重要因素,集成开发环境、社区和厂商的支持、普及程度等都是开发人员在选择编程语言时无法回避的问题。很多时候,以如此细致、理性的标准来决定使用哪门语言是没有意义的,因为开发人员受到环境的各种限制,老板的命令、公司的成见、团队的意见、硬件的配置、IDE 的支持、学习曲线的陡峭程度等等因素,都会影响开发者对编程语言的取舍。那么,我们学习松本行弘的设计思想有什么意义呢?在语言学有一个 Sapir-Whirf 假说,认为语言可以影响说话者的思想。计算机语言同样可以影响开发人员的思考方式和由此产生的代码。Ruby 语言在设计方面的考量和选择,能够帮助开发人员以更广阔的视角和更高的层次来看待软件开发中遇到的问题,解决的思路可以更加多样化。即使开发者的语言各种各样,Ruby 的思想在软件开发的设计、编码方面仍然有宝贵的参考价值,《松本行弘的程序世界》是一个学习的切入点。


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评论

发布