Archive for the ‘软件开发’ Category

MVC:用来打破的原则

2011/09/07

去年的《 C 是 MVC 的 C 》简单写了些 MVC 的基本原则,特别是关于 controller 这个经常被忽视和误解的模块。有人认为对 controller 的不同认识叫做 MVC 的不同「流派」或者「变种」,我不能认同。我相信只有符合《 C 是 MVC 的 C 》里所述原则的 MVC 是降低整体复杂度的最佳方案。另外,我最近在读《 The No-Designer’s Design Book 》,并且向没机会系统学习设计的 UI 开发者推荐这本书。为了在一本书的篇幅里让 no-designers 把握设计的关键,它没有讨论什么不同的「流派」,而是把产生好设计的要素归结为四大通用原则。

有人可能会对上面两件事产生同样的质疑 —— 面对一个博大的领域,拘泥于几条有限的原则,是不是纸上谈兵,削足适履?我这篇 blog 就是想写些实际开发中遭遇的细节与原则之间的冲突。对于这种冲突,《 The No-Designer’s Design Book 》有两句精彩的总结:

  • You must know what the rule is before you can break it.
  • Either do it all the way or don’t do it. Don’t be a wimp.

UI 程序的设计开发就是关于遵循和打破 MVC 原则的权衡,而这个权衡恰恰说明了坚持唯一原则的重要性。我相信在唯一原则下有意识地考虑取舍优于在几个所谓 「流派」之间选择或者摇摆。

反面向对象的 Model

设计模式产生于面向对象的方法。但 MVC 最有意思的一点是 model 的设计原则体现出反面向对象思想。面向对象的第一要旨是数据和行为的统一。Model 表示一组数据,对于一组数据来说,最重要的行为有两类,一是在某种场景中体现自己,二是对外界的交互产生反应改变自己。MVC 模式里,第一类行为被划分为 view 的职责,第二类行为被归为 controller 的职责。Model 呢?Model 就是一组数据而已。Model 模块被剥夺了「对象」的特质。MVC 的理想状态是回归到「数据」加「算法」的传统模型。从另一个角度看,我经常说面向对象技术不适合 UI 之外的领域。作为 MVC 中和 UI 本身关系最远的模块,model 印证了这个观点。

主要行为被 view 和 controller 剥夺之后,model 勉强保留了一个行为 —— 保持数据的一致性。在《无毒副作用》中提到过,保持一致性并非本质上不可或缺的能力,没有冗余的理想 model 无需任何行为就能天然地保持数据一致性。所谓冗余就是指 model 中的一部分数据总是可以被另一部分数据计算出来。那些不能从其它数据计算出来的最基本的数据即「自由变量」,被计算出的数据即「依赖变量」。保持一致性的行为就是用自由变量重新计算和更新依赖变量的操作。在理想条件下,「依赖变量」应该作为 view 重新绘制时的临时数据而非 model 的一部分。

但现实中 model 终归无法避免冗余,所以本质上不必要的行为在实际中是不可或缺的。保持数据一致性的行为是 model 设计中细节复杂度的主要来源。比如在复杂耗时的数据变换中,如何在达到最终数据一致的过程中,找到保持局部一致性的中间状态,并且通知 view 进行平滑的显示。这在《并发与并行》中有所讨论。Model 是理想中的傀儡,在向现实的妥协中成长为一个负责的对象。

性能与一致性

在计算机领域,最伟大最纯粹的设想往往只为一个原因妥协 —— 性能。MVC 也不例外,它的理想是通过牺牲性能来降低整体复杂度:无冗余的 model 从根本上杜绝数据不一致的问题。一旦需要,view 就遍历整个 model,计算所有绘制所需的「依赖变量」,最后重新绘制界面。每个 UI 的实现,为了避免 premature optimization,都应该从这个状态开始。但是当 UI 真正发布的时候不可能还保持这种干净的状态。UI 迭代开发过程的重要部分就是不断根据需要在 model 中添加「依赖变量」来满足 view 的性能需求。引入「依赖变量」会增加 model 数据不一致的机会。如果为了抵消这种风险而过多添加和调用保持数据一致的代码,反而会导致性能损失。开发 UI 的整个过程是仔细权衡数据冗余和一致性级别的调优过程。也许构建强大的服务后端难度不亚于建设一个电站,但构建精致的 UI 也不亚于设计一台精巧的引擎。

如上所述,model 的设计要考虑几种平衡:

  1. 如何以及在何种程度上引入「依赖变量」。
  2. 在何时进行保持数据一致性的工作。其可能的时机包括 controller 的事件处理,view 的更新,后台 thread 或者 timer。

这些决定会影响 controller 和 view 的处理时间,而 controller 和 view 的处理方式恰恰是不同平台处理方式差异最大的地方:

  1. 平台会以什么样的频度对 controller 和 view 分别发出事件?比如,在不同系统中,mouse move 的频度有很大区别。
  2. Model/controller 通知 view 更新的机制会被如何处理?在有些平台上,这种通知是不可靠的, 在频率过高的时候会被 view 忽略,而在另一些平台上,每次通知都会得到 view 的完全处理。

所以,UI 的用户体验问题经常会从这种平台差异中显现。虽然没有统一的方式来解决它们,但是有一个很好用的简单方法来判断问题的类属:如果在  controller 或 view 的处理函数中稍稍改变处理频度(比如三次调用中的两次处理成 no-op 直接返回),问题的症状会发生变化甚至消失,那么基本上就是数据冗余和保持一致性的级别问题。对于如何设计「依赖变量」,不光要考虑冗余数据量的多少,也要考虑冗余的层次,即某些「依赖变量」是由其它依赖变量计算而来,为高一级的依赖变量。在需要快速响应的情况下,不更新或者只更新少量低一级的依赖变量,同时尽量使用之前计算好的依赖变量。在可以放慢响应速度的时候再更新各级依赖变量。

另一方面,某些「自由变量」在加入时间约束之后,可以简化为更少量更精简的数据,也就是在 model 暂时存入「变化量」,让 model 暂时维护 change 而非纯粹的 state,可以进一步减少 controller 的处理时间。从这个意义上说,自由变量是时间和变化量的「依赖变量」,在 model 中暂存变化量是对自由变量的「延迟存储」,是另一个方向上的冗余。

背离理想状态是为了更好的处理某些操作,但背离的程度越高,MVC 系统的弹性就越差。MVC 的弹性表现在可以处理设计者预料之外的用户操作。理想状态的 MVC 可能会因为性能低下而表现出响应不够敏捷,或者显示的更新不够平滑,但是不会陷入不一致的状态,甚至是除非关闭程序否则无法恢复的状态。而增加数据冗余的系统面对用户的随意操作其崩溃的危险也会增加。关于理想 MVC 系统弹性的讨论详见《 C 是 MVC 的 C 》。

Model 的 View 还是 Viewer 的 View

View 是 model 的 view 吗?回想一下别人或者你自己解释为什么系统中要有多个 view 的时候是否举过这个例子:有一组数据,又要显示成直方图,又要显示成饼图,又要做成表格 ⋯⋯  这个例子里,view 是 viewer 的 view 而不是 model 的,因为 view 的类型是为了满足 viewer 的解读而不是 model 的结构。但是,作为 viewer 的 view 面临一个难题,viewer 需要的并非总是直方、饼、表那样泾渭分明的形式,他可能需要一个表格里有饼图,另一个饼图里有表格。另一方面,model 里的某些数据可能在多个 view 里都要显示成同一种形式。

如果把 view 按照 viewer 的解读形式来组织,那么在处理混合显示时就可能不得不在多个 view 模块里加入重复的代码。有人说给不同 view 加上 common base class,建立 view 的继承树。我不相信这种方法指导下产生的代码是可持续维护的。另一个诱惑是把这些代码移到 model 中,让数据自己显示自己。但 model 夺取本该属于 view 的行为是对「关注隔离 (separation of concern) 」的违背。而且,MVC 系统只有一个 model,让它背负 view 的责任容易让 model 需要实现的操作数量面临组合爆炸 (combination explorsion) 的困境。

所以,除了 viewer 的 view,在复杂系统里还必须实现 model 的 view。为某些数据编写负责显示它们的 view 模块,让 viewer 的 view 来重用。在有些 UI framework 里有这种 model 的 view,比如 Java Swing 中的 renderer。这些模块始终都应该是 view 的一部分,而非 model 的行为。除了「维护数据一致性」,不要随意给 model 增加任何行为,因为「维护数据一致性」本身已经是背离理想状态的产物。

最成功的对象

如果说面向对象在 UI 之外的领域是失败的,那么反过来考虑,如果 view 都不能作为「名副其实」的对象那么面向对象这门技术也就一无是处了。而 view 确实在某种意义上是最「成功」的对象,它的行为最复杂,同时也拥有数据。

View 的行为复杂不必多说,它的绘制代码是用户直接接触的整个应用最炫的部分。但 view 不适合直接拥有数据,因为那是 model 的责任。那么 view 如何拥有数据?答案是:MVC 是可以嵌套的,而这种嵌套就直接作用在 view 上。在嵌套形式中,内层 MVC 作为外层 MVC 的 view 的一部分,外层 view 操作或者直接作为内存 MVC 的 controller ,外层 view 的一部分或者全部绘制行为是从外层 model 向内层 model 搬运(经过转换的)数据。经典设计模式中的「组合模式 (composition patter) 」的一些实际应用就是多层嵌套 MVC。常用 UI framework 提供的现成 control 通常就是局部的微型 MVC 系统。嵌套 MVC 是 view 的主要数据构成,也是 model 数据冗余的另一种形式,维护一致性的时机一般在最外层 view 的绘制操作中。

所以,当系统的某一部分过于复杂但又有很强的内在联系,不必强求把数据和行为分拆到系统的最外层 MVC,而是可以把复杂的部分实现成内层 MVC。代价是内外层的 model 之间会有一定程度的数据冗余。

MVC 模式的三个模块,controller 趋于分散(见《 C 是 MVC 的 C 》),model 趋于纯数据结构,view 强于行为,有时也间接拥有数据。MVC 和面向对象是共同产生的,它们的外延也趋于一致。MVC 看似描述了三个对象,其实是一个对象拆开的三个部分。

微内核领域的传说

2011/08/05

IT 业和自然科学领域常说的「传说」一词来源于英文 myth ,是个负面的形容词,更接近「流言」、「谣言」的意思。如著名的电视节目《流言终结者》(mythblaster) 。也经常被翻译成「神话」,如著名的《人月神话》(Man Month Myth) 。这篇文章不是要贬低微内核 (micro-kernel) 这个概念本身,而是说人们对这个领域中的很多东西存在不小的误解。

Mac OS X 是微内核

OS X 的内核叫做 XNU ,是一个基于 Mach 和 BSD 的内核。因为 Mach 是最早的微内核,所以人们自然而然地认为 XNU 也是一个微内核。在软件领域「基于」这个概念是很模糊的。甚至一个系统只是从另一个系统的设计中借鉴了一些思路,也可以称为「基于」。虽然我相信 Windows NT 和 VMS 没有一丝一毫共同的 code base ,但还是认为前者「基于」后者。XNU 确实是继承了 Mach 和 BSD 不少的 code base ,但 XNU 抛弃了 Mach 最核心的设计选择 —— 微内核。所以即便二者有大量共同的 code base ,也只能勉强称为「基于」。

为什么继承了 Mach 大量 code 的衍生产品 XNU 反而抛弃了被继承者的核心设计?这要回头看 Mach 是如何开发的。开发一个全新的内核是件非常困难的事情。在内核的基本功能完成之前,开发者的工作几乎都是不可见的。Linus 在《 Just for Fun 》里回忆,到第一个 shell 被成功移植到 Linux kernel 上之前,他都看不到自己的工作有什么成效,突然( shell 移植完毕),似乎一切一下子完成了。这种最后突现的情形,对软件开发来说风险很大。因为错与对要到很晚才能揭晓。Mach 的开发采取了另一种渐进的、随时可以检验成果的方式:

  1. 取一个完整的 BSD 内核,改名叫 Mach 。
  2. 在这个内核上开发一套底层 IPC 机制。一般 UNIX 的 IPC 机制会利用内核模块的功能,比如 I/O ,内存管理,等等。这在微内核上是不推荐的,因为像内存管理或者 I/O 这类功能在理想的微内核上要作为用户态 (user-space) 服务。所以微内核的 IPC 非常底层。在 Mach 上是基于 message 和 port 的消息队列通信。
  3. 把原来的 BSD 内核各模块间通信(主要是方法调用)替换为 message/port 通信。
  4. 把已经改为通过 message/port 通信的模块从内核态 (kernel-space) 移出到用户态。

XNU 的做法是 undo 了第 4 步(有时连第 3 步也 undo 了),保留底层 IPC「机制」(mechanism) ,抛弃了 Mach 的微内核策略 (policy) 。通过这么做 XNU 拥有一个独特的优势 —— device driver 可以采用两种状态来运行,运行在用户态的时候利于开发和调试,运行在内核态的时候又不会引入微内核的开销。前者基本上只用来提高驱动开发的效率,实际发布版的 device driver 总是运行在内核态。所以,OS X 的内核 XNU 不是微内核。讨论微内核的成功与失败,性能高低等等问题,OS X 都不是一个适当的例子。

微内核解决了 Monolithic 内核不能解决的问题

往往认为微内核解决的最大问题是 monolithic 内核的复杂度和可维护性问题。可实际上微内核和 monolithic 内核在复杂度上并无显著差别,甚至后者还简单一些。从 Mach 和 OS X 内核 XNU 的例子可以看到,加上第 4 步,得到的就是微内核。去掉第 4 步,得到的就是 monolithic 内核。去掉第 3 步,得到的就是一个进一步简化了许多的 monolithic 内核。进一步去掉第 2 步回到原来的 BSD 内核,仍然是一个设计良好的内核,这也是 Mach 能在其上循序渐进的开发成功的原因。

其实,代码运行在内核态还是用户态不是构建其上的系统整体复杂度的决定因素,决定整体复杂度的关键在于各部分代码之间的功能边界是否清晰。微内核的作用在于强制约束了各个功能之间的边界,并且提供了有限的保护,也就是利用硬件 MMU 进行内存保护(但是这种保护并不真的总是有效,由于模块的 bug 造成的 crash ,常常并不能仅靠重启该模块来修复,而是必须重启整个微内核和所有服务,所以一个单独模块在内存保护之下 crash 所造成的危害并不见得一定比整个内核 crash 来的小)。这种约束的代价并不低廉。实际上,微内核本来的雄心是把 task schedule 和 IPC 之外的功能都放到用户态,但是由于硬件设计的限制(很多硬件其实都是按照 monolithic 内核的需求设计的),性能因素,以及内存保护能提供的实际错误恢复能力,实际中以微内核自许的产品 —— 比如 MINIX —— 一般也只能把 device driver 这样的外围模块放到用户态。提供强制的设计边界固然是好的,但是只有开发社区达到很大规模,这种需要付出代价的强制边界才能真正带来价值。小规模的开发社区更容易自律,从而更好的发挥 monolithic 内核本身结构精简的优势。而 device driver 的开发从来不是一个从业者人如潮涌的领域。

另一方面,monolithic 内核并不是真的铁板一块。Monolithic 的定义是不「强迫」开发者把模块放到用户态。「不强迫」不是「强迫」的对立物,而是后者的「超集」(superset) 。Monolithic 内核并不会阻止开发者把模块放到用户态,它所要求的是开发者自行开发一个小规模的内核态 stab 来负责把用户态模块的操作 relay 到内核。FUSE 就是一个在 monolithic 内核中实现用户态 driver 的例子。

所以微内核只是一个试图规约开发者的规范。它能解决的问题,并非 monolithic 内核所无法解决的,只不过侧重的灵活度角度不同。

微内核的效率很低下

这是个很有争议的问题。有些微内核将处理系统调用 (system call) 的模块也做成了用户态服务(仅仅是处理系统调用,而不是这些调用真正代表的功能。这里的处理系统调用模块只负责把用户态应用的请求分发到实际的功能服务)。即使是一次什么都不做的空系统调用,也要导致四次上下文切换 (context switch) 。

  1. 从发起系统调用的进程,切换到内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  2. 从内核态切换到处理系统调用的服务。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)
  3. 从处理系统调用的服务切换回内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  4. 从内核态切换回发起系统调用的进程。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)

常见的 monolithic 内核一般只需两次无需内存映射表的切换的上下文切换( 1 和 3 )。而且,由于处理系统调用的模块往往并非真正提供服务的模块,所以对微内核来说非空的系统调用会有另外的两到四次切换,而对 monolithic 来说只是一些普通的函数调用。作为 monolithic 内核 XNU 无需切换 2 和 4 ,但《地址空间划分》中介绍过,由于比较独特的地址空间,需要在切换 1 和 3 时切换内存映射表,所以开销会稍大。有 benchmark 测试曾经以 XNU 处理空系统调用慢于 Linux kernel 来说明微内核的效率低下,其实是误解了这个细节差异。

支持上面这个理论分析的实际性能数据大多都是在 Mach 微内核上得出。但是有人指出 Mach 是微内核的第一次尝试,设计和实现失误很多。比如很多的性能开销 (overhead) 其实发生在消息的合法性检验操作上,而非上下文切换。特别是用 Linux kernel 改写而成的 L4 微内核的支持者认为 L4 更好的精简了消息 IPC 机制,更多的利用了 x86 架构的独特硬件特性,性能和 monolithic 内核不相上下。

在我看来,这是「 Java 的性能已经和 C++ 不相上下」的另一个版本。不论怎么说,微内核引入的开销是客观存在的,更多细心精巧的设计也只能是降低这个开销,不可能完全消除。而微内核的安全保护,只有当内核的开发社团拥有一定规模的时候,才能体现出其价值。可是作为核心底层的模块,内核和 device driver 的开发永远是一个小团体的世界。特别是在 Mac 和 mobile 这类软硬件结合的生态体系重新获得成功,替代了以 multi-vendor 为特征的 PC 生态体系之后。

黑客的闲暇

2011/07/12

有时候空闲下来不知道该干些什么。这时要是能找到一个有意思的问题来研究会感觉很充实。若面对的不是纯粹知识理解的难题而是一个有实际需求的问题,并且又能做出点什么东西满足这个需求,那心情就更好了。但更多时候是,即使闲着难受,也会觉得时间太短不够解决意料之中和意想不到的困难而难以鼓起勇气真正动手。

上周一整周都在休假,其中不少时间花在读剩下的少半本《 Snow Crash 》上。这本书断断续续读了一年半的时间。这类小说让我兴奋的地方在于看过之后,再面对普通工作时感觉那些东西多多少少带了一点向 cyber 世界慢慢前进的味道。主人公 [1] 用代码构建和打破 Metaverse 的规则或者拿着电脑 hacking 是我最喜欢的情节。讲到 hacker 和 hacking [2] ,作者 Neal Stephenson 是个行家,虽然这是本带有很大政治和文化成分的科幻小说而不是像《 Hackers & Painters 》那样的专门讨论 hacker 文化的文集,而且是在 1992 年的 IT 石器时代 [3] 写成,但是其中蕴含的很多 hacking 的精髓是这两年我才渐渐了解的。这是我近一年来越来越喜欢这本书并且决心在一周里读完剩下半本的原因。

在小说里最后一场大战开始之前,Hiro Protagonist 乘坐的 Mafia 游艇被 Raven 所在的潜艇击沉,他们一行四人的救生艇飘荡在太平洋上。一个无聊的 hacker 会做的自然是 Hacking !

Hiro does not know what he is doing, what he is preparing for. That’s okay, though. Most of programming is a matter of laying groundwork, building structures of words that seem to have no particular connection to the task at hand.

Metaverse 是个三维的虚拟现实世界。不过在救生艇上,为了延长电池续航,Hiro 特地进入了自己的纯二维工作空间 Flatland。让我觉得自己为了节省电池在 09 款的 MacBook Pro 上 re-login 的切换到集成显卡是个挺酷的举动。读完整部《 Snow Crash 》,我最喜欢救生艇上这段情节。因为背个笔记本电脑到处乱跑就是我在大学读书就有的梦想。写完一个简单的工具之后,Hiro 开始准备在 Metaverse 里最重要的工具,一个『隐身』的 avatar :

To write a really good invisible avatar from scratch would take a long time, but he puts one together in serval hours by recycling bits and pieces of old projects left behind in his computer.

什么都从头写起也许是有些人刚出道时的野心,不过对大多数干过几年的程序员来说,有东西能被『 recycle 』并且能用这些零碎『 put one together 』才是真正的资本。所以,在有时间有精力的大项目里要 leave behind 什么东西,才能有朝一日在『救生艇』上 put together 一件趁手的兵器?这是个严肃的问题。

除了看书,休假的这一周还受朋友之托要写个简单的小工具。我没有大块时间可用。倒是如上所讲,有些时间看小说,不过休假前半个多月的工作到了这样的程度,那些时间不用来看看小说其实也支撑不了什么脑力活动。但答应的东西要做,好在不是什么和 Apple 竞争的商业界面。我决定用而且只用 Adobe Bridge 的 ExtendScript [4] 来搞这件事情。编码和读小说是交替进行的,当读到 Hiro 救生艇 hacking 这段的时候,小说和现实仿佛重叠了。最后我终于 put together 出一个还说得过去的东西。作为正式工具来说太粗糙了。我觉得很大一部分原因是以前从未真正考虑为这样的『 put together 』leave behind 什么像样的东西。

我考虑从现在开始应该为 leave behind 做点什么准备。看来最终在『救生艇』上 put together 一定是用『高级动态语言』来完成。但太粗糙的东西不行。最后用『高级动态语言』拼接的『 bits and pieces 』必须是称手的、精致的。所以得用『集成污染』最少的语言 [5] 打造最好的模块。所以,我要再深入的学习 Lua ,而且开始考虑给我的所有 code base 加上 Lua 集成。

脚注:

  1. 主人公就叫 Protagonist。
  2. 这里的 hacking 和 security 没有直接关系。是《 Hackers & Painters 》里说的那种 hacking 。
  3. 1990 年的时候父母要给我买台电脑,我为他们乱花钱感到极大的惊恐。1993 年我有第一台电脑,以为要解决问题必须用 BASIC 来编写程序。
  4. 一个 JavaScript 的定制版本。和普通的 JavaScript 不同,ExtendScript 运行在 Bridge 而不是 Web 浏览器中。DOM 也和一般操作 HTML 和 CSS 的 DOM 完全不同。它基本上是用来编写桌面 UI 的。
  5. 集成污染

容忍度的拐点

2011/06/13

For the average user this new kind of [Web] software will be easier, cheaper, more mobile, more reliable, and often more powerful than desktop software.

—— Hackers & Painters, The Other Way Ahead

The intended role for the Macintosh was as a client operat-ing system for nontechnical end users, implying a very low tolerance for interface complexity. Developers in the Macin-tosh culture became very, very good at designing simple interfaces.

—— The Art of UNIX Programming

我有个朋友最近忙着补救项目中走弯路浪费的时间。在项目早期,他们的 team 里有过一次大争论:UI 是全部采用 native 代码还是部分采用 Flash 。最终的结果是 Flash 一派占据了上风。理由是大客户相信 Flash 的『成功案例』。当然,像我朋友这样做过『消费者』应用的人认为所谓大客户的意见都是『企业开发』的狗屎。

事情的发展是,虽然 Flash 方案确实比 native 方案有更多开发便利(比如更方便的内存管理,更可靠的 exception 处理,以及从上一个『大客户企业方案』中继承下来的部分 code base ),但是在这个项目里反而拖累了开发进度。整个 UI 没法形成一个整体。在早期争论中被祭出的『大客户』也渐渐让位给习惯了 Apple 产品和 Photoshop 之类软件界面的挑剔消费者。走过一段弯路之后,整个项目重新回到纯 native 方案。

这让我回想起 2001 年前后,我的程序员生涯中最『惬意』的一段。因为客户很『 resonable 』,我可以经常告诉他们这个或者那个『没法做』。为什么呢?因为客户相信 Java ,他们的整个 IT 基础建立在 Java 上,他们要求我们用 Java 。所以,当你证明了 Java 不能做一件事情之后,你就证明了宇宙的最高法则。所以,是这些家伙自己放宽了自己的容忍度。

最近有一本 Paul Graham 的作品集《 Hackers & Painters 》在国内出版,网上讨论和引用很多,总体来说这是本不错的书。其中第二篇《 The Other Way Ahead 》把 Web 技术描述为软件开发未来的趋势。如果审视一下这个『未来的趋势』,我们会发现同样的问题。你想关闭某个文本框的拼写检查,而在另外一些文本框里打开吗?Web 做不到。如果你无意中按住鼠标拖过页面,划过的文字可以不被高亮选中吗?Web 做不到。希望你的『 Web app 』支持 undo/redo 的快捷键吗?Web 做不到。你的 Web app 可不可以有一个真正的『护栏』防止用户做某些从任何意义来说都不合时宜的操作而不是仅仅规劝或者提醒吗(比如点浏览器的 Back 键)?Web 做不到。 ⋯⋯ 当然,用户不会因此特别不满意。他们会说:这是个网站,有什么办法。我经常听到有 Web 开发者说:我们能掌控整个 server ,所以我们可以自由选择合适的高级语言来完成任务,而不像可怜的 native 应用开发者。他们错了,是用户放弃了对他们的应用的品质的要求,用户认为那是个网站。

作为单独 app 的 Google Earth 和作为『网页』的 Google Map 有什么不同?那就是下面这种处理网络延迟的情形在任何一个 app 的 beta 版里都是不能容忍的:

面对这些问题,乐观的 Web 开发者经常说『情况已经不是那样了』或者『马上就不是那样的』。是啊,从 canvas 这样的机制引入 HTML5 可以看出 Web 提高用户体验的努力(但是暂且别说 canvas 的支持目前有多么脆弱)。可是,用户中最挑剔的那部分的容忍度也是变化的。以 version control 这个程序员的工具来说,以前字符界面就很好,今天很多人在用矢量图界面来处理复杂的 branching ,也许明天就会有 3D 化的 UI 。软件的一个基本要求是根据用户操作改变界面的状态,比如在 Word 里改变一下行宽,或者在 PowerPoint 里修改阴影,或者按 Cmd+T 让 Firefox 创建一个新 tab ,用户一开始对这些变化没有过多要求,所以软件都是从上一个状态突变到下一个状态;然后,用户开始要求在状态变化中显示一些重要的中间态;今天,大多数变化都要求用平滑的动画显示。而 Web 技术在追赶这些不断收紧的容忍度时,其实已经失去了一部分作为『未来趋势』的优势。比如,所谓 watching users 的优势,只对于像 CGI 那样每个操作触发一次 client-server traffic 的应用才有用。为了追求用户体验而不断引入 client-side script 和 AJAX 的 Web ,在这方面早就慢慢和 native 应用拉平了。

回顾技术的发展,其实没有什么焕然一新的方案能够在不牺牲用户体验的前提下大幅度减轻程序员的工作。那些所谓的快速开发手段,只是用 PR 攻势来催眠用户放宽容忍度。这个是 cross-platform 。那个是『云』。用这些概念来降低用户的期待。当然不否认,某些技术提供了一些当时用户急需的便利,可以认为它们要求用户放宽容忍度的方式在一定期限内是合理的。关键在于,开发者往往看不到用户为便利付出的代价(而且常常是程序员独有的便利)。当一些开发者抱着用户和开发者都获得了『免费』便利的错觉时,这种技术就成了他们眼里『未来的方向』(这个词是我从《 Hackers & Painters – The Other Road Ahead 》某些段落的字里行间中读到的。当然《 Hackers & Painters – The Other Road Ahead 》这篇文章里有很多观点是建设性的,比如对软件演进和技术支持和对团队士气的看法。对 Web 过于乐观的态度,考虑到其写作的时间也情有可原。只能说,如果作者到今天还是这些观点,就略显自大了)。开发者要避免这种自大,要清楚自己和客户为了一个方案各自付出了什么,用户何时会失去容忍度。失去这种自知之明是某些 Web 时代的人面对 native 程序回归时表现出吃惊的原因。

高级动态语言与软件业 —— 交流与内省

2011/06/01

一直以来,我都认为编程语言是程序员之间的语言,是人和人交流的工具。主流编程语言最重要的特性是清晰、准确,开发者个人在使用中是否感受到的灵活、简洁、优雅则是次要的。这是把编程语言和同为交流工具的自然语言比较,同时又因为两者构建文化认同的程度有所不同,所得出的结论。

接触高级动态语言也已经有近四年,一直觉得虽然它们并不适合作为程序员交流的工具,但绝对是程序员的工具箱里不可缺少的一样东西。几个月前写了一篇《高级动态语言与软件业》,主要观点是,语言概念相对简单,用法相对固定的静态语言更适合构建商业级别的产品,而高级动态语言只限用于符号推导系统。之后是对 Lua 的学习和《集成污染》。高级动态语言并非我所能认同的交流工具,但又具有不可否认的必要性,我经常重新考虑『编程语言/自然语言』这个类比应该如何涵盖高级动态语言。

今天和别人讨论 Objective-C  vs. C++ ,突然回忆起到半年前看的《 What Technology Wants 》里对自然语言的描述:

The creation of language was the first singularity (注1) for humans. It changed everything. Life after language was unimaginable to those on the far side before it.  …  But the chief advantage of language is not communication but autogeneration. Language is a trick that allows the mind to question itself; a magic mirror that reveals to the mind what the mind thinks; a handle that turns a mind into a tool.

这时我想到自己从未把自然语言的『自省』功用和编程语言联系到一起。也就是上文引用里的所谓『 question itself 』,『 reveals to the mind what the mind thinks 』。如果把『自省』这个元素考虑在内,『编程语言/自然语言』这个类比就更完整。复杂的产品中用户定制和核心算法的符号推导部分更加接近『自省』而非『交流』。而且,很多有经验的程序员都说过,你不一定要用它来开发产品,但是学习一门高级动态语言能让你更了解编程的本质。这和自然语言的『自省』功用更接近。最后,自然语言的『自省』功用大于『交流』,编程语言的『自省』功用也很重要,但应该退居『交流』功用之后。因为毕竟程序员不是机器,他们的思考大部分还是通过自然语言完成的。软件产品也不是机器人,很多功能进化依然是通过用户反馈、迭代开发加更新发布而非简单的定制完成的。

脚注:

  1. 顺便说一下,在读到这段文字之前,尽管我知道『技术奇异点』之后和之前就像人和动物的区别,但是我从来没直接意识到自然语言就是史前的奇异点。

集成污染

2011/05/27

写过《高级动态语言与软件业》之后,Alex 回复说 Lua 语言应该符合我的期望。其实我应该早就意识到 Lua 的存在,因为 Lightroom 就是用它作为 policy 部分的编程语言。只是 Lua 的库不如 Python 等语言强大,作为编写应用程序的主力语言还显单薄。但正是因此使用轻量级的 Lua 的开发者很少受到诱惑用它来开发复杂算法和 policy 之外的东西(比如 UI ),那正是我在《高级动态语言与软件业》里所反对的。

另一方面,作为描述算法和 policy 的动态语言的另一个重要条件在《高级动态语言与软件业》里讨论不多,却正是 Lua 的优势 —— Lua 是我所知的造成『集成污染』最少的库。

什么叫做『集成污染』?简单地说,如果系统的构建( build )在加入某个模块之前一直好好的,加入这个摸块之后就开始出现问题,这就叫集成污染。这种症状在编译、链接和运行阶段都可能发生。例如,编译阶段的集成污染的可以来自 C 的 macro ,typedef ,条件编译,C++ 的 template 。运行时的集成污染可能来自不同的多线程模型混用。

一个模块能否被顺利地集成到更大的系统中,取决于它希望别的模块如何对待它。一个模块对其它模块的要求越多,越不明确,集成的困难就越大。当这些要求被错误地理解或者无法满足的时候,就变成了集成污染。如果一个模块只是对数据的类型有所要求,那么任何一个可以定义抽象数据结构的静态语言都能很容易的描述这种要求,而且也很容易得到满足。但是,即使是如此简单的需求,也会遇到命名冲突之类的问题。通常这就是 macro 和 typedef 造成集成污染的原因。

接下来,有些模块的要求在『理论上』能够被满足,但是这种要求超出了现有技术的表达能力。C++ template 和多线程就是这样的例子。一个使用了 template 的模块很难用 C++ 语言本身的结构来说明自己的需求。对多线程来说情况就更严重,因为不仅编程语言不能描述线程模型,连数学语言和自然语言也很难清晰描述一个模型对线程使用的期望。所以,基本上集成涉及多线程的模块是一种盲目试错的过程。

更进一步,有些模块的要求是否能被清晰地描述已经不重要,因为这些要求常常是难以满足的。这些模块要求独占 system-wide 稀缺资源。比如,Cocoa 框架把整个应用的很多初始化工作和主消息循环封装到一个黑箱函数 NSApplicationMain() 中,古老的 C++ Framework 如 MFC 也会隐藏 main() 函数。这些库用自己的方式任意使用消息循环和主函数。如此霸道的模块,在系统中是一山不容二虎。《高级动态语言与软件业》提到很多高级动态语言和静态语言的互操作方式仅限于单向发起 —— 能调用静态语言编写的动态链接库,但是不能作为库被其它(静态)语言调用,这就等同于对主函数(有时连同消息循环)的独占。

解决集成污染的方法是各种边界分离。像 macro 、typedef 之类的命名污染可以通过文件分离来解决。终极方法是进程分离加 IPC 。不论如何难缠的集成污染也会在进程边界止步。另一种解决之道是参与集成的模块低调、收敛、谦虚一些。这就是 Lua 的方式。采用 ANSI C 中最成熟的跨平台部分,避免了编译时的错误。放弃对线程和 I/O 的支持,避免了运行时污染。Lua 能做这样的取舍,原因在于有合理的目标,把自己定位为描述算法的符号操作语言,和 hosting 应用的交互限于严格而且有序的符号集合。这让 Lua 成为扩展 C 语言和的方案中最简洁和最可靠的形式。

无毒副作用

2011/04/21

每当提到如何『评价』面向对象技术时,很多人的直接反应就是上升到世界观层次。说它让软件设计和客观世界更好的一一对应云云。我非常怀疑这种思考方式。客观世界是多维度的,面向对象的软件系统只能选取一个维度进行描述,同时为兼顾其它维度要做各种 leaky 的妥协。因此,对一个客观场景总有很多个可能的面向对象『设计』存在,每种『设计』都不能被认为是错的,甚至不能被认为是差的。这种一对多的情景很难让我信服面向对象技术比其它手段能更『自然地』描述世界。对我来说『自然地』只有一种标准 —— 容易取舍的单一选择。

面向对象技术没有脱离面向过程和命令式( imperative )的思维。有经验的 C++ 、Java 、Objective-C 程序员在阅读和编写代码时能熟练到近乎反射地把 this 或者 self 作为一个参数来思考(比如 0->func() 这样的调用为什么在某些情况下是可行的),或者把函数 override 作为函数指针的另一种形式(比如 0->func() 为什么在某些情况下是不行的)。这里没有任何世界观的升华。面向对象真正的便利仅限于消除负面副作用( negative side-effects )。

举个例子来说明什么叫负面副作用。假设在程序中有个二维的『单位向量( normalized vector )』,其方向任意,长度必须为 1 。这个数据通常表式为 x 分量和 y 分量。作为单位向量,必须满足 x 和 y 的平方和始终为 1 ,这种条件叫做对象数据的不变式( invariation )。不小心打破不变式,比如把 x 和 y 都设成了 1 (平方和变成了 2 ),这种行为就叫负面副作用。在数据暴露给所有算法代码的编程方法中,不变式是通过文档由程序员人工保证的。面向对象方法认为人工保证容易出错,所以通过隐藏对象的数据,只允许有限的成员函数访问它们来保证不变式。这样需要精心维护的涉及不变式的代码仅限于少量成员函数。类似的例子还有如何表示一个钢体(内部没有复杂结构,各部分的相对位置不会变化的物体)在三维空间的位置和姿态。钢体本身的形状一般用一组顶点表示,它在三维空间的位置和姿态用一个 4 x 4 的变换矩阵表示。熟悉 3D 几何的人知道,4 x 4 矩阵可以表示拉伸、平移、旋转、甚至扭曲等任意的变换。但是对于钢体来说,只有旋转和平移是有物理意义的。所以必须对这个数据结构中的 4 x 4 矩阵做出限制,即钢体对象的不变式。

从另一个角度来看,面向对象保证不变式的方式未必是理想的。首先,人工约束不变式并非那么难。Linux kernel 核心数据结构的不变式就是通过活跃的讨论和 code review 保证的。其次,人工约束不变式可以让设计者天然地保持警觉,把核心数据结构的数量最小化。最后,不变式可以通过合理的设计数据结构来减小甚至消除。比如上面单位向量的例子里,可以把单位向量用一个夹角 —— 即它和坐标系的某个轴的夹角来表示。这样的表示法不再需要额外维护不变式。类似地,在钢体的例子里用一个平移向量和一个旋转向量代替 4 x 4 矩阵表示位置和姿态,可以减少要维护的不变式。

这两个例子都说明,不变式来自数据结构的信息冗余。当然,表示复杂的对象很难做到完全消除信息冗余。若底层 framework 的 API 直接提供 4 x 4 矩阵运算或者 (x, y) 二维向量运算,我们就要考虑在这样的系统上构建对象的简便性和复杂度,未必要舍近求远用平移、旋转和夹角来表示。同时,保持冗余信息也是提高性能的手段,很多操作都需要 x / y 分量或者 4 x 4 矩阵,如果不直接存储它们,就要在每次需要的时候临时计算。所以造成不变式的信息冗余又来自两处:一是性能优化的需要;二是支持我们构建对象的底层系统中可以被直接操作的模块粒度不够小。这就像现实中我们没法构建一个无需仓库的物流,也无法用纳米亚原子直接打造机器。

前面提到用非面向对象技术消除副作用并非难如登天。另一方面,复杂系统的信息冗余不可能完全消除决定了面向对象技术多少是有用的。这里的结论是不要把一种消除副作用的便宜之计吹捧成改变世界观的灵丹妙药。特别是在一叶障目之后,容易忽视背后的更本质更有效的手段。很多吹捧面向对象技术的人往往喜欢把系统的核心数据结构设计的非常庞大,信息非常冗余。从编写复杂的保持不变式的代码中获得不真实的成就感。这种过度吹捧在面向对象以外也有例子。并行化技术最大的负面副作用是不同线程同时修改一份数据,即 race condition 。消除这个副作用有几个方法,其一是被诟病的 lock 技术。Lock 的问题在于为了试图保证数据的不变式引入了时序上需要额外保证的不变式。在《多核与锁》里有所讨论,这里不过多涉及。

接下来是两种直接保证并行处理中数据不变式的手段。一个方法是只用栈变量,因为栈是每个线程独立的。更精确的说是只使用 per-thread 数据,因为有些 per-thread 数据其实在全局堆里,只是通过 per-thread 的引用来管理,而且后面马上说到有些系统的 per-thread 数据不是基于栈。这方面有一个最近成为热门的例子 —— GPU 运算。OpenGL 的 GLSL( shader language ),CUDA 和 OpenCL 的编程语言处理的数据都仅限于当前 GPU core 使用的 per-thread 变量。另一种方法是根本不用变量,那就是一小撮人推崇备至的函数式编程( functional programming )。在《并行计算的解药》里我讨论过,前一种手段里分割输入数据才是关键。而后一种,我认为根本是步子迈的有点大,扯着了。对这两种手段的评价,都只能限于消除副作用的影响,解决问题的真正关键在于更高层次的数据分割。

一项技术技术是否能被主流软件开发社区采用,不在于世界观和哲学那类东西,而是更实际的 —— 在消除副作用的同时保持原有的思维方式;至少增加的额外思考不能超过熟练程序员的反射弧。要求改变世界观的技术一开始就失败了一半。

土气

2011/04/07

自从成为 Mac 用户之后,我一直非常想把 Firefox 从系统中清除出去。原因有三个:第一,我基本不用 add-on ,偶尔重度使用的几个功能也都有 Safari extension 或者 Chrome extension ;第二,从 code base 的角度说,我对 Webkit 的信任度大于 Firefox ;第三,Firefox 太『土气』。前两条我认为尚属个人感觉。这第三条从道理上说主观的不能再主观,可我相当肯定这种感觉。和 Safari 相比,Chrome 经常给我欠修饰,不圆润的感觉,但绝对没有『土气』。可看到 Firefox 就和看到上世纪 80 年代风格的服饰一样,而且是那种绝不会借复古卷土重来的土气。

最近发现一例,是对正在下载图片的处理(注意这里的例子是正在下载但还没有结束也没有彻底下载失败的图片)。Safari 是直接留白(强调一下,Safari 对彻底下载失败的图片的处理并非留白)。

Firefox 的处理是从上世纪不变的黑框加『圆、方、三角』图标。

这两种效果有什么区别?不用讲太多道理,第一感觉就是 Firefox 的效果太『土气』。我和同事曾经比较过一些应用的界面。一个可拖动的分割线( draggable separator )是单像素宽的黑线还是多个像素宽的伪 3D 效果,这点区别就能决定一个界面是整洁还是凌乱(猜猜哪种让界面更整洁)。这种感觉不易量化,但绝对客观存在。不管有什么理由,『土气』就是一种缺陷,就意味着你得花额外的精力从其它方面弥补。

Firefox 的『土气』带来了什么收益?它告诉了用户一个在今天的网络状态下持续不会超过两秒钟的状态(为了抓上面那张图我费老了劲了,真想找个让网速临时慢下来的工具)。去掉这个状态的可见性不会令用户丝毫不便,刻板的显示这个状态给用户带来的只有一闪而过的『视觉污染』。就这个例子来说,Firefox 的设计不仅仅是土气的问题,它忽视了 UI 设计的一个基本原则,不要浪费用户的注意力。用户的『注意力』是稀缺资源,这种知道不知道两可的程序内部状态无需让用户费心。这是 Firefox 对拨号时代的遗产长久不加审视的体现。我不敢说从这个例子可以总结出什么一般规律,但是我怀疑那些表面上看着土气的设计也总是隐藏着对可用性的某种深层次的忽视。

逻辑的残影

2011/03/31

作为一个棋力不高,但尚可作为消遣的普通人,我认为棋力的高低体现在头脑中能预先演算多少步棋。不过,听说即使是高手演算时也常会犯一种错误。举例来说,当演算到第三步的时候把一个子移开,但是演算到第五步的时候会下意识觉得那个子还在第二步的位置。这叫做『残影』棋子。

在做编程这样的复杂工作时,也经常在逻辑概念的变换中构建『残影』。最近就因为一个『残影』两天没有睡好觉。先不说犯错的经历,讲讲相关的概念本来应该是什么样子。计算机图形系统的终极目标是生成二维图像,这个图像可能会显示在显示器上,或者存储成文件,可能是一幅静态图片,也可能是一组图片构成的动画,可能是一幅照片,图案,也可能是一个虚拟 3D 场景的影像,总之终归是一个(或者多个)二维图像。生成二维图像需要预先开辟绘制的空间。这种空间一般显卡的显存中创建,因为这样才能使用 GPU 硬件加速。OpenGL 里对这种空间的抽象叫做 Framebuffer Object( FBO )。

我在《 OpenGL 随想》系列里说过 OpenGL 是一个重度依赖 context 的系统( Cocoa 的 Quartz 和 Windows 的 GDI+ 也依赖 context 。Quartz 的概念模型来自 PDF ,所以文档和打印系统也依赖 context 。由此可见一切图形相关系统都建立在 context 这个基础概念之上)。Context 这个概念有多重要,多基础,就体现出我后面犯的那个错误多愚蠢。FBO 是 OpenGL context 中的一部分,一个 context 可以拥有多个 FBO 。其中 ID 为 0 的 FBO 用于屏幕显示。

如果严格如上所述,那么这个模型是优美的。可是,OpenGL 设计之初只考虑到屏幕显示,FBO 在 OpenGL 2.1 和之前都不是标准的一部分,只是一个扩展( FBO EXT )。所以屏幕显示并不是真的被称为 0 号 FBO 。创建所谓『 0 号 FBO 』的过程和创建其它 FBO 的过程大不相同,所需的参数都直接传给创建 context 的函数。就是这个陷阱构建了逻辑『残影』的陷阱 —— 我把 context 当成了对『 0 号 FBO 』的封装。

正在开发的程序对 OpenGL 的应用都是 offscreen 渲染。所有的 onscreen 显示都在 OpenGL offscreen 渲染之后用其它方式完成。因为开发刚开始的时候在 Windows 平台上没来得及搞清如何使用 FBO EXT ,所以在 Windows 上的 offscreen 渲染也用的是『 0 号 FBO 』( Mac OS X 版本从一开始就使用了 FBO EXT ),必须创建一个不可见的窗口并将其 DC 句柄作为『 0 号 FBO 』传给 wglCreateContext() 。由于『 0 号 FBO 』和显示硬件紧密绑定,所以对高级需求有一些不可接受的限制。比如,普通 PC 显示器的单个色彩通道深度不超过 8 位,用『 0 号 FBO 』就没法生成 16 位色彩深度的位图。这时我开始在 Windows 上研究使用『真正的』FBO EXT 。

工作了一夜之后,我开始认为『真正的』FBO 解决了问题,而最初构建的那个逻辑『残影』从此开始作祟。因为把 context 当成了 『 0 号 FBO 』的封装,同时有了『真正的』FBO,我毫不犹豫的删掉了创建 context 的代码,包括在 Windows 上创建隐藏窗口和调用 wglCreateContext() 的代码,和在 OS X 上创建 CGL context 的代码。

结果当然是悲剧了。为了让过程更悲壮,删掉创建 context 代码之后的程序居然在一台 Windows PC 上和所有 Mac 上运行正常(除了一个不怎么被测试到的 case )。这个结果当然让我的『 context 不过是 0 号 FBO 的封装』的歪曲理论更加坚固。随着程序在其它 Windows PC 上产生垃圾结果,以及那个失败 case 被发现,我的头脑也跟着崩塌了。早上还在和同事夸耀删掉了创建 context 的『无用』代码,晚上就陷入了呆滞。

第二天早上醒来,迷迷糊糊中突然想起了什么是 context 的真实含义。早晨同事见到我的第一句话是『我觉得是删掉 context 的缘故』。知道这件事的美国同事也来信说他猜想是 context 的问题,尽管他只看到了运行结果没有看过代码。看来大家晚上都在思考啊(虽然美国是白天)!

回头分析,凭空构建出『残影』固然在于我因为时间紧迫没有仔细思考概念,但是很大程度上也是因为 OpenGL 的 API 由于历史原因没能清晰和正交的反应真正的概念。各个 FBO 本该在概念上平等,而且完全不用『 0 号 FBO 』的情况也是合理的,那么从 OpenGL 的状态机概念来讲,FBO 应该在 context 创建之后挂靠其上,而不应该和创建 context 的函数有任何语法上的直接关系。但是实际创建 context 的函数( CGLCreateContext()NSOpenGLContextwglCreateContext() )都需要一个事前创建的『 0 号 FBO 』作为参数,哪怕这个『 0 号 FBO 』完全没有用处。

并行计算的解药

2011/03/21

前几天看到 reddit.com 的 programming 类别第一名是《 Parallelism is Not Concurrency 》。读完之后发现和我去年的《多核与锁》有很多观点上的共通之处。《 Parallelism is Not Concurrency 》的开篇行文更流畅幽默,对并发( concurrency )和并行( parallelism )有更精辟的总结。比如:

Concurrency is concerned with nondeterministic composition of programs (or their components).  Parallelism is concerned with asymptotic efficiency of programs with deterministic behavior. Concurrency is all about managing the unmanageable. … Parallelism, on the other hand, is all about dependencies among the subcomputations of a deterministic computation.  The result is not in doubt, but there are many means of achieving it, some more efficient than others.

谈及『依赖( dependency )』在并行计算方面的关键地位之后,《 PINC 》进行了一个有趣的推理:

  1. 因为依赖是个关键概念而且它是高于硬件的抽象概念,所以我们需要一个基于语言的并行计算模型;
  2. 因为依赖阻碍并行,所以我们需要一个消除了依赖的并行计算模型;
  3. 由 1 和 2 ,我们需要一个能最大限度消除依赖的编程语言;
  4. 所以,functional 编程语言是解决并行计算的终极形态。

这个四步推导其实非常脆弱。首先看第 2 步,它的结论是需要一个『消除了依赖的并行计算模型』,这与《多核与锁》中谈到并行计算需要把数据分割成没有依赖关系的独立块的说法是一致的。但是,『消除依赖』是指在数据中消除依赖,是指对问题本身,或称为『问题域』消除依赖。因此准确的推导应该在第 3 步中提及的『最大限度消除依赖的编程语言』之前加上『在问题域中』这个修饰语。但是,很不幸,第 4 步的 functional 编程语言只是在编程阶段,或者称为『实现域』中消除了程序员引入依赖的能力。

人们经常想当然地把『消除』视为『消除烦恼』的同义词。但是,只有在问题域中的消除才基本保持这个同义性。而在实现域中编程语言消除程序员的某种能力更贴近于『强迫』。『强迫』的好处在于让程序员更自觉的用更好的更清晰的方法考虑问题。前提是,程序员已经拥有更好的更清晰的方法,只是经常限于时间和精力图省事抄近路采用一些丑陋的方法。这个前提在很多时候是成立的,比如说,程序员都能轻易的避免使用混乱的 goto ,而另一方面,他们又都倾向于随意使用 goto ,所以,一种禁止使用 goto 的编程语言是好的(当然,C 并没有禁用 goto ,而是把 if-elsewhilefor 等等分支功能做得易用而且优美,从而比粗暴的禁止更好发挥作用)。

很明显,这个前提在并行计算中不成立!程序员还没有一种真正通用的方法可以在问题域消除影响并行的依赖。假设一个程序员精通各种顺序排序算法,唯独没有学习过 Quick Sort 算法。那么即使学会 functional 编程语言之后,这个程序员也不可能自动的把原来的顺序算法改写成适合并行的形式。消除依赖需要在问题域的艰深研究而不是实现域编程语言的强迫。研究万有引力需要高深的微积分工具,而其结论万有引力公式只需要简单的代数知识就能在大多数场合应用。类似的,一旦在问题域中取得了消除依赖的突破,传统的编程语言往往也能很轻易的实现并行。例如并行编译这个问题,因为 C 语言在设计的时候就保证了单个文件在编译时没有对其它文件的依赖,所以并行编译只要简单的多运行几个编译器进程就能实现。(不要把这里的『设计语言』和原文四步推导中的『语言』混淆。对于编译来说,C 语言的设计是问题域而非实现域。)这时形式上消除依赖的 fucntional 编程语言倒是多余的了。

另一个例子是 3D 图形处理,这个问题包含大量的数据计算,比如顶点位置。而每个顶点的位置可以独立计算没有相互依赖。因此业界可以为这个问题建立消除依赖的硬件模型 —— GPU( 同样,这里的消除是『强迫解除能力』的意思,因为问题域本身天然地免除了依赖 )。这就给《 PINC 》中的第 1 步推导提出了反例,对天然免除了依赖的问题是可以建立基于硬件的模型的。而另一方面,因为 GPU『强迫解除』了通用 CPU 的很多能力 —— 比如要求数据的小粒度分割和缺乏对分支的支持,当业界想把 GPU 用于通用并行计算的时候,遇到的最头疼的问题是如何把原来在 CPU 上运行的程序改写成适合 GPU 的形式。当 functional 编程的拥趸苦恼 functional 语言的小众化现状的时候,其实已经有了一个流传广泛的低级 functional 编程语言,这就是 GPU 及其接口 OpenGL/OpenCL/CUDA 。这个模型已经在大多数 PC 上(包括 Mac )甚至很多 mobile device 上得到推广,实现了 functional 编程拥趸们在推广度方面的梦想。但是奇迹并没有自动出现,在问题域还是遗留了众多的工作要做。

所以,解决并行性能的关键,不在于急匆匆的在编程语言级别剥夺程序员引入依赖的能力,而在于研究更多问题的本质,去掉那些直观上存在但是并非本质上不可消除的依赖。实现域的『强迫解除』并不能带来问题的自动解决。等问题域的依赖被解除了,再考虑语言级别的问题不迟。即使到了那个时候,我也不认为 functional 语言会成为主流,因为和用来取代 gotoif-elseforwhile 相比,functional 语言解决问题的方式还是十分丑陋的。