松本行弘的程序世界
Table of Contents
1. 关于Lisp宏
虽然Ruby非常重视扩展性,但是有一个特性,尽管明知道它可能带来巨大的扩展性,我却一直将其拒之门外。那就是宏,特别是Lisp风格的宏。
宏可以替换掉原有的程序,给原有的程序加入新的功能。如果有了宏,不管是控制结构,还是赋值,都可以随心所欲的进行扩展。事实上,Lisp编程语言提供的控制结构很大一部分都是用宏来定义的。
所谓Lisp流,其语言核心部分仅仅提供极为有限的特性和构造,其余的控制结构都是在编译时通过宏来组装其核心特性来实现的。这也就意味着,由于有了这种无与伦比的扩展性,只要掌握了Lisp基本语法(S表达式),就可以开发出千奇百怪的语言。Lisp提供了在读取S表达式的同时进行语法变换的功能,这就在实际上摆脱了Lisp的S表达式舒服,任何语法的语言都可以用Lisp来实现。
那么我为什么拒绝在Ruby中引入类似Lisp那样的宏呢?这是因为如果在编程语言中引入宏的话,活用宏的程序就会像完全不同的专用编程语言写出来的一样。比如说Lisp就经常有这样的现象,用宏编写的程序a和程序b,只有很少一部分是共通的,从语法到词汇都各不相同,完全像是用不同的编程语言书写的。
对程序员来说,程序的开发效率固然很重要,但是写出的程序是否具有很高的可读性也很重要。从整体来看,程序员读程序的时间可能比写程序的时间还长,读程序包括为理解程序的功能,或者是为维护程序去读,或者是为调试程序去读。
编程语言的语法是解读程序的路标,也就是说我们基本可以不用追究程序或库提供的类和方法的详细功能,但是,“这里调用了函数”,“这里有分析判断”等基本的常识在我们读程序时很重要。
可是一旦引入了宏,这一常识就不再适用了,看起来像是方法调用,而实际上可能是控制结构,也可能是负值,也可能有非常严重的副作用。这就需要我们去查阅每个函数和方法的文档,解读程序就会变得相当困难。
我知道世界上有很多Lisp的程序员并不受此之累,他们正是通过面向特定程序员定制制语言而最大限度的提高了开发效率,不过在我看来他们只是极少数的一部分程序员。
我相信,作为在世界上广泛使用的编程语言,应该有稳定的语法,不能像随风飘荡的灯芯那样闪烁不定。
2. 颠倒的构造
如果仔细想想,就会感到很不可思议,为什么程序员非要像计算机的奴隶一样工作呢?我们到底是从什么时候开始放弃了主宰计算机这个念头的呢?我想其中一个原因是阿尔法综合症。阿尔法综合症是指在饲养宠物狗的时候,宠物狗误解了一直细心照顾她的主人的地位,反而感觉到,他自己是主人,比主人更了不起。
计算机也不是好伺候的,系统设计困难重重,程序有时也会有错误。一旦有规格变更,程序员就要动手改程序,程序有了错误,也需要一个一个纠正过来。所以在诸如此类繁琐的工作中就会发生所谓的尔法综合症现象,主从关系颠倒,程序员沦为计算机的奴隶,说的客气一些,也顶多算是计算机的看门狗,这难道是人性使然吗?
不,不要轻易放弃。人是万物之灵,比计算机那玩意儿要聪明百倍,当然应该摆脱计算机奴隶的地位,把工作都推给机器来干,自己尽情享受轻松自在。因此,我们的目标就是让程序员夺回主动权!程序员如果能充分利用好计算机所具有的高速计算能力和信息处理能力,有可能会从奴隶摇身一变,像变戏法一样完成工作,实现翻天覆地的大逆转。
但是想要赢得这场,夺回主动权的战争,武器是必需的,那就是本书所要讲解的语言和技术。
3. 结构化编程和面向对象编程
结构化编程通过整理数据流,提高了程序的生产效率和可维护性。同样面向对象编程,通过对数据结构的整理,提高了程序的生产效率和可维护性。
如果把面向对象编程看作是对结构化编程的扩展,那么对象是否是现实世界中具体物体的反应就不重要了。实际上面向对象编程语言中的对象,向字符串,数组和范围等,很多都是没有现实世界中的具体物体与之对应的。即使现实世界中与有具体物体与之对应,对象也只是描述现实物体某一侧面的抽象概念而已。比如猫有颜色血统等很多属性,而程序中的对象并不需要把这些属性都考虑进去。程序只是处理抽象数据。
关于继承,规格继承和实现继承的区别也是非常重要的话题。规格就是从外部看到的类的功能,这样的继承就是规格继承。实现继承是指继承功能的实现方法。面向对象编程语言是一下子把规格和实现都继承下来,在最近的编程语言中,有的是把这两种继承分开了,比如Java里的接口就是规格继承。(Ruby通过Mixin来解决多重继承问题)
4. 克服动态类型的缺点
动态类型的缺点主要有三个,即在执行时才能发现错误,读取程序时可用到的线索上,以及运行速度慢。 首先执行时才能发现错误,这一点可以用完备的单元测试来解决,如果能严格实行完备的单元测试的话,即使没有编译时的检查错误,程序的可靠性也不会降低。
其次,读程序时可用到的线索少,这一点可以通过完整的文档来解决,Java有Java到技术到技术,可以在源代码中同时写文档,减轻维护文档的负担。
最后运行速度慢这一点,随着计算机性能的提高已经不太重要,现在的程序开发中程序的灵活性和生产力更为重要。
5. Haskell函数式语言
与一般的语言不同,具有延迟计算性质的编程语言在字面上的意思与实际处理是不一样的。不进行多余的处理,即使是无限连续的数据也能处理,这是这种语言的优点。另一方面,程序的语句与实际值实际的处理不是直接对应的,一旦出一点什么问题,很难判断问题出在什么地方,这是这种语言的缺点。
Haskell利用多态性的类型系统和类型推测,在维持着接近于duck typing灵活性的同时,也使编译时的完全类型检查成为可能。但是,如果类型推测出点问题,程序出了类型错误的话,就会出现令初学者感到恐惧的错误。
6. 垃圾收集的两个问题
认为垃圾收集慢的观念只有一半是对的。计算机不可能完全理解人的意图。某内存区域是否不再使用,计算机不可能完全正确判断出来。本来就是因为人不能判断才会发生那种问题的,跟计算机说“把所有不再使用的内存都找出来”也属于无理要求。
这样的话就只能使用一些别的方法来找出不再使用的内存。比起人工直接管理内存区域的寿命,在不要的时刻在明确释放,这需要做些多余的工作,所以人们都认为执行时间会变。但是即使在人工明确释放内存区域的场合,内存管理也还是需要一定开销的。有研究表明在某些条件下,垃圾收集比手工管理内存还快。
垃圾收集可靠性低的观念,可能是由于个别垃圾收集处理程序的质量不高而形成的。如果垃圾收集本身有程序错误的话,这些错误就可能导致前面列举的各种内存问题。实际上,我在Ruby的垃圾收集中也有几次因为程序错误,而给大家带来了很多麻烦。
但是想一想的话,不使用垃圾收集的时候也经常会发生内存问题,程序本身的可靠性随之降低,性能也就无从谈起。结果常常是程序员在无可奈何的时候,只好自己想点办法来实现类似于垃圾收集的功能。与其每次独自实现垃圾收集,真不如利用已经实现好的垃圾收集,这样才可以最终得到最大的好处。
Java语言的存在打破了这两个偏见。1995年登场的Java从一开始就具备垃圾收集功能,以后又经过持续不断的性能改善,扭转了垃圾收集慢得不能使用的偏见。在Java之后诞生的编程语言,不管是否受到Lisp的影响,几乎都毫无例外的拥有垃圾收集功能。垃圾收集从诞生之日起,经过了40多年的发展,终于成为大家认可的一项特性了。