顺着 Java 、Perl 、Python 一路看去会发现一个有趣的规律。至少那些 Lisp 高手会发现。后一个比前一个更 Lisp 一点。
If you look at these languages in order, Java, Perl, Python, you notice an interesting pattern. At least, you notice this pattern if you are a Lisp hacker. Each one is progressively more like Lisp.
—— 《 Revenge of the Nerds 》
从 Lisp 说起
好好学一门高级动态语言一直是日程上一项安排,但限于精力未着手施行。最近看了以《 Revenge of the Nerds 》为首篇的一系列讨论 Lisp 的文章,很受启发。联系我现在的工作领域,对高级动态语言的应用有些看法和期待。虽然在入门之前妄谈可以被称为『成见』,不过鉴于深入学习并非很小的投入,预先思考一下也有益处。
算法与系统
上面提到的这些文章当然都在正面评价 Lisp ,我也多少赞同。但是个人经历形成的感觉仍然是,Lisp 这样的高级动态语言(或者说以 Lisp 为终极目标不断进化的动态语言)并不会在短时间内 —— 估计十年内,成为解决软件业主要问题的手段。我不想老生长谈『性能』问题。性能的确是个问题,但我认为除此之外还有更重要的问题,在更长时间之内不会被基本解决的问题:动态语言适合编写复杂的『算法』,但不适合编写复杂的『系统』。很难给『算法』和『系统』做形式上的定义或者区分。《 Re: Revenge of the Nerds 》中引用了 Trevor Blackwell 的大致说明:
我认为,在 Web 崛起之前,绝少有程序包含比排序更复杂的算法。你认为 FreeBSD 里有复杂的算法吗?Nortel (我靠,老东家!)交换机的五千万行代码里可能连排序都没有,全是硬件资源管理和错误处理。Cisco 路由器的上百万行代码也没有几个复杂的算法。
Before the rise of the Web, I think only a very small minority of software contained complex algorithms: sorting is about as complex as it got. Can you think of a complex algorithm in FreeBSD? The 50 million lines of code in a Nortel switch probably didn’t even contain a sort. It was all about managing hardware and resources and handling failures gracefully. Cisco routers also have millions of lines of software, but not many complex algorithms.
高级动态语言适合执行简洁符号的复杂变换和推导。这类问题具有令纯脑力工作者愉悦的挑战性,属于『优雅』的问题。因此,哪怕能让这些问题的表达和解决更优雅一点点,动态语言的设计者和拥护者也会真诚地付出巨大努力。另一方面,复杂系统没有令人愉悦挑战性,包含无数细节,属于脏活累活。这是高级动态语言不擅长的或者说不屑于的。
优雅的,在智力上具有挑战性的,纯粹的符号问题可以由一个人或者一个很小的团队独立完成。为解决这类问题设计的语言更注重问题本身的表达,是高度简洁和脱离具体平台的,但是其互操作性也受到限制。
复杂的充满细节的工作则需要整个业界协作。需要芯片级别、系统集成级别、操作系统、库、和应用不同级别的互操作。这种复杂的互操作不可能通过简洁的符号来完成,取而代之的是 C 级别的 application programming interface 和 application binary interface 。曾经认为以摩尔定律带来的性能提升和业界的标准化努力能逐渐消除这类细节化的工作,但是实际趋势说明这种预计是错误的。首先是对应用的需求总是超过摩尔定律的发展,今天的 mobile 应用和 3D 应用是以前难以想像的。二是人们和计算机的交互总是不断脱离已经定义的符号范围,几十年前是命令行,几年前是菜单和按钮,今天是各种 drag and drop ,scrolling ,gesture , multi-touch ,未来将是 Kinect 和我们现在还想像不到的东西。
高级动态语言能承担的不过是整个系统中可以被简单符号定义的那部分,正如很多文档中把高级动态语言编写的算法部分叫做 kernel(不要和操作系统 kernel 混淆),只是一个小小的硬核。核内代表优美的数学推导。而外围的复杂系统才是把符号对应到真实世界的建模过程,充满了微妙而且要反复变换的权衡、妥协、近似。
通用和专用
上面所说的高级动态语言的强项限于纯符号问题只是它不能主导软件开发的一方面原因,另一方面,在纯符号问题领域高级动态语言也不是高枕无忧。低级静态语言不适合表达复杂的算法。但是这不表示它们不能。《 Revenge of the Nerds 》也承认,只要是图灵完备的语言都能等价地描述任何算法。当然该文也嘲笑了一番这种做法,它说,一般的方案是:
使用一种高级动态语言,- 用(当前适用的静态语言)写一个高级动态语言的 ad-hoc 解释器(为了应付当前的问题凑合实现的不完整功能),
- 程序员自己充当高级动态语言的人肉解释器。
这种说法似乎成立,当程序员由于种种原因不能使用高级动态语言但是又需要实现复杂算法时,为了使设计清晰不得不自行搭建一层抽象(程序的或者人肉的)。于是,这似乎证明高级动态语言是必需的,只要这种情况是一直成立的。
但是情况会发生变化,当复杂算法足够通用以致为众多领域所需时,软件业对这个算法的投资会达到无需中间抽象而直接用静态语言表示的程度。正如快速傅立叶变换和 H.264 解码有成熟的硬件实现方式,而且不仅仅是几个具体的产品,而是一种脱离具体产品的业界普遍掌握的定式,不需要任何程序或者人肉的抽象。这些通用算法的复杂度虽然很高,却已经不需要『算法 » 高级表示 » 低级等价表示 » 硬件表示』这样的推导过程,而是完全跳过中间步骤。就像小学生每天笔算多位数乘法而不会从十进制定义的角度思考竖式计算原理。定式像生物为了快速行动而做出的不经过逻辑推导的反射行为,也许逻辑是更优雅的智慧结晶,但是反射也是进化的杰作。
我们经常说硬件的发展会减少低级语言的应用领域,但是忽视高级动态语言的应用领域也有类似的时效性。后者的阵地同样不断受到挤压,最终只剩下和各个行业的专业知识紧密相关的专业算法,比如原文中提到的航空管制系统。这些领域原本被认为是 domain-specific language 的适用范围。而动态语言进入这个原本认为应属 DSL 的领域并不奇怪,因为 Lisp 这样的语言里充满了可以把自身调教成另一种形式的 feature ,比如 macro 。按照编程的命名文化,这些创造 feature 的 feature 可以称为 meta-feature 。
虽然动态语言有临时构建类 DSL 语言的能力,但是这样临时搭建的 DSL 必然是千差万别没有标准的,所以用动态语言写成的代码只适合小范围的解决专业问题。。而对于需要大量互操作的问题,对问题本身的优雅和简洁的表示就会退位于对适应不同文化的交流的需求。这就必然退位于概念更为简单,更为固定的静态语言。这方面的反例是试图在通用领域引入 meta-feature 的 C++ 。把过于灵活的语言应用到整个系统,就像现代人和古人用『几何』、『骚人』这样的词语直接对话。
不完美的子语言
我并非用高级动态语言不能成为软件业的主要工具来否定它的前景。相反,我认为一切未成为大众消费级需求的符号变换操作都应该用动态语言编写。需要使用动态语言的领域会越来越多。另一方面,用高级动态语言承担超出这个范围的应用,尤其是在 I/O 和 UI 方面,虽然在产品初期规模较小的原型阶段可能很顺手,但是随着维护期的增长、代码量的扩大和团队的扩大,最后的净收益很可能为负。
我期待的高级动态语言的模式是类似 SQL 的 sub-language 模式,简化到完全省略 I/O ,嵌入到其它语言或者应用中,只关注符号操作。正如 Emacs 和 AutoCAD 提供的 Lisp 接口。可惜这种做法并没有盛行,没有什么高级动态语言真正的努力支持过 SQLite 那种以库的形式在静态语言中 in-process 解释的能力。采用高级动态语言作为算法模块的系统通常要借助 IPC 或者 Web server 来完成互操作。这也让高级动态语言不得不提供 I/O 甚至 UI 支持,成为扬短避长的累赘。而 IPC 在性能和复杂度上的开销也让很多应用用静态语言来凑合描述本该用动态语言描述的算法( ad-hoc / 人肉解释器)。
我并非 monolithic 的鼓吹者,但是我倾向于把 process separation 作为设计原则而非结构强制。比如,micro-kernel 的强制 user-space device driver 并不成功。而 monolithic kernel 允许设计者自行决定 driver 的结构成就了 FUSE 这样的 user-space driver 。高级动态语言作为一种不能独当一面的语言,最好还是把这种决定权交给应用开发者。高级动态语言并非风格一致的拒绝 in-process 形式,它们通常可以调用静态语言编写的动态链接库,但这种 in-process 交互是单向的,如上所述,动态语言的解释器很少能作为库被其它应用调用。这种单向交互是因为高级动态语言的拥护者很少把自己看作不能脱离静态语言生存的核心,相反,他们把静态语言看作是 legacy ,而且认为高级动态语言才应该是系统的 master 。
这里我们转头看看那个最初来自《人月神话》的著名论断:不论用何种语言,程序员的生产力以『行代码/天』计算的话不会有很大变化,『 bug 数/行』也不会有很大变化。是的,这个论断和我迄今为之的经历没有任何矛盾之处。在我接触过的系统里,程序员用更简洁的语言编写模块会更快,这些模块的 bug 更少,即使有,修改起来也更快。但是,如果系统出现跨模块的问题,若是涉及高级动态语言编写的模块,解决起来会比不涉及这种模块的情况难度高上几个数量级。问题不在于引入高级动态语言模块本身,而在于高级动态语言目前的实现的拙劣的互操作机制 —— 笨重的 IPC ,繁复的参数和数据结构转换。如果这一切有所改观,那么高级动态语应该可以释放更强大的生产力。
发表评论