PLAI笔记
我看的是中文版本 https://lotuc.gitbooks.io/plai-cn/content/ 可以下载到英文版本。这本书使用的语言是Racket plai-typed方言,我在自己的DrRacket里面没有找到,所以好多例子也没有跑。我感觉这本书写的比较松散,不太适合作为自学材料。我简单地翻了一下每节,我觉得自己对程序语言里面的大部分议题都稍微有所了解。书中对宏,CPS以及类型系统有单独三章来讲述,而且篇幅都比较长,应该是比较重要的内容。我对这些议题都是浅尝辄止,大概知道怎么回事,稍微深入就不行。但是我也不打算去深入学习它,脱离实际语言来学习这些东西,我觉得有点头晕。
书里面谈到了Mixin的引入和保守垃圾回收,我觉得写得不错,所以摘抄下来放在这里。不知道是不是有点“买椟还珠”的感觉。
在Java中当我们写下一个“类”时候,那对大括号中事实上是什么东西呢?它不是完整的类:完整的类取决父类,那又递归的取决于它的父类。其实,我们在大括号内定义的是类的扩展。仅当在这个定义中加入父类后,它才是个完整的类。 自然我们要问:为什么?为什么不把扩展的定义和将扩展应用于基类这两个行为分开呢?即,将这段代码:
class C extends B { ... }
分割成:
classext E { ... }
和
class C = E(B)
其中B是某个定义好的类。看上去这样好像只是用更长的代码实现一样的东西。但是这种类似函数调用的语法不禁让我们浮想联翩:可以将某个扩展“应用”于多个不同的基类。比如说:
class C1 = E(B1); class C2 = E(B2); // ...
诸如此类。通过将E的定义和其扩展的类分离开,我们将扩展从固定基类的暴政中解放出来。这种扩展有个名字:mixin。“mixin”一词起源于Common Lisp,是多重继承的特定使用模式。
Mixin使得类定义具有更好的组合性。它提供了很多多重继承的好处(重用多段功能代码),但是避免了多重继承的麻烦(例如没有前面讨论的复杂的查询顺序问题)。采用去语法糖的方式的话,mixin还非常容易实现。Mixin基本上就是“类的函数”。我们的目标语言支持函数,而且已经确定了类去语法糖后的表达式,该表达式可以放入函数中,这意味着实现简单的mixin模型非常容易。
在静态类型语言中,好的mixin设计完全可以改善面向对象编程的实践。假设我们要定义一个基于mixin的 Java。如果mixin等效于类到类的函数,那么这个“函数”的“类型”是什么?显然,mixin应该使用接口(interface)来描述其输入和输出。Java支持后者(但不强制要求),但是不支持前者:类(的扩展)扩展的是另一个类——这个类中所有的成员对扩展都是可见的——而不是其接口。这意味着子类获取了父类所有的行为,而不是其规范。如果修改父类,就有可能导致子类出错。
在支持mixin的语言中,我们就可以这么写:
mixin M extends I { ... }
其中I是接口。这样M可以用来扩展实现了接口I的类,语言能保证只有I中指定的成员在M中可见。这就遵循了好的软件设计的重要原则之一。
“面向接口编程,而不是面向实现(Program to an interface, not an implementation)” —— 《设计模式》
mixin解决了库设计中出现的一个重要问题。假设我们有十几个不同的特性可以用不同的方式进行组合,我们应该提供多少个类?更甚之,并不是所有特性都可以相互组合。显然,产生所有组合对应的类不现实。更好的方案是允许程序员选择他们关心的特性,且提供必要的机制防止不合理的组合。这正是mixin所解决的问题:mixin提供类的扩展,程序员可以自行组合,而接口必须要能对上,从而创建自己需要的类。
Racket的GUI库中广泛使用了mixin。例如color:text-mixin的输入是基本的文本编辑器接口,输出是彩色的文本编辑器接口。后者本身也是一种基本的文本编辑器接口,于是其他基本文本相关的mixin还可以继续应用于其输出。
Mixin也有局限:只能进行线性的组合。这种限制有时会给程序员带来不必要的负担。将mixin泛化,不是只对单个mixin扩展,而是扩展一组mixin,这被称为trait。当然,允许扩展多个就必须要处理潜在的名字冲突。因此实现trait 必须同时提供解决名字冲突的机制,通常是某种名称组合代数。Trait是mixin的补充,程序员可以自行选择最满足其需求的机制。Racket支持mixin和trait。
一个令人兴奋的研究方向——称为保守GC——成功的为此类语言创造了足够高效的GC系统。保守(conservative)GC背后的基本原则是,尽管理论上每个贮存地址都可能属于根集,但实际上它们大部分都不是。它会通过一系列聪明的观察来推断出哪些位置肯定不是引用(这点和传统GC相反),然后将它们安全地忽略掉:例如,在字节对齐的体系架构中,奇数值不可能为引用。通过忽略大部分贮存,通过对程序行为作出一些基本的假定(例如程序不可能产生某种类型的引用),并且小心操作不去修改贮存(例如,不改变值中的比特,不移动数据)的情况下,可以得到一个还算有效的GC策略。
保守GC在那些使用或者依赖C和C++实现的编程语言中比较常见。例如,早期的Racket就完全依靠它。这是基于以下原因:
- 它是种便捷的自举技术,语言实现者能得以将精力集中在其它更富革新性的特性上。
- 如果语言能控制所有的引用(比如Racket),那么可以使用便于提高GC效率的内存表示法(例如,用1填充所有(真正的)数的最低有效位)。
- 它使得该语言和C以及C++实现的库交互变得容易(当然前提是这些库也符合该技术的要求)。