推荐一篇文章:构造健壮系统

有时无知是福。俺看到一点新鲜的科普也能觉得造化神奇。刚才读Gerald Jay Sussman(SICP作者)的文章,Building Robust Systems – an essay,竟然心如小鹿乱撞,手心湿润,仿佛第一次握住初恋情人温柔的手。

这篇文章主旨明了:构造复杂的健壮系统非常困难。我们的软件能够有效完成某件具体任务,却不能适应业务领域的变化。一点细微的需求或部署的改动都能让我们的系统变得脆弱。反观生物进化的历史,无数生物在频繁变更的严酷大自然里繁衍生息。也许我们能从中学到让系统随环境变更而自动演化的秘密。当然,不是所有的软件都身段僵硬。35年前问世的Emacs至今是最高效的编辑器之一(不怕惹众怒地说一句,流行的EditPlus和UltraEdit除了学习曲线,还是不能和Emacs比),实属居家旅行杀人越货之必备利器。20年前问世的TeX ,1989年就停止更新代码,但这并不能阻挡它横扫科技出版业,变成科技出版物排版的事实标准,基本上干掉了科技出版业里的排版员这项本来很有前途的工作。不过,这些都是特例。我们需要了解的是构造复杂系统的通用方法。不然skynet和matrix怎么能够问世嗫?

Sussman在文章里讨论了生物在残酷进化里得来的五坨特性。这些特性对谙熟生物的老大们也许不是新鲜事,却能让我这个靠三思科普生物的半文盲肾上腺素急剧释放:

  • 冗余(redundancy)和简并(degenerate,不知道这个翻译对不对)
    强健的生物系统都有大量冗余。比如肾和肝。割掉一个肾,我们仍然能活蹦乱跳。把肝的三分之一供奉给乙肝病毒,我们被歧视乙肝携带者的公司气死的几率也大多得肝癌挂掉的几率。

    生物系统也高度简并:同一功能可以由不同的部分来完成。比如我们的能量既能从糖获得,也能从脂肪获得,还能从蛋白质里获得(这个造成我们为节食和消耗热量绞尽脑汁,实属进化跟不上变化的特例,另当别论)。而这三者的代谢过程都不一样。甚至我们的遗传代码也是简并的。我们有20来种氨基酸,但核苷酸构成的密码子组合却有64种。这样才能让点突变不至于影响某个密码区的蛋白质,使得突变能积累起来,同时不会导致明显的表现型后果。不然领导某天生出一特聪明的小孩儿(变异了),但就是长得像头羊,我们非得抓狂不可。简并成于进化,也成就进化。环境改变了,大不了某个部分废掉,但生命继续(系统依然满足规格)。而废掉的功能为变异(或修补)腾出空间。整个系统没有被惊扰。

    现在的软件系统往往包含冗余,但很少刻意加入简并。作者顺便对Python拍砖:Python的口号是TIOWWTDI TIOOWTDI(There is only one way to do it),明显是零简并系统。
  • 探索行为
    探索行为也是生物系统健壮的基石。生物需要的合适功能通过“生成-测试”的机制获得。系统某部分生成功能,而另外相对独立的部分测试功能,决定接受还是拒绝测试结果。比如说支持细胞上的微管阵列决定细胞的形状。微管们总是不断被生成和摧毁。那些有幸碰到细胞膜里稳定子的微管们得以存活。最终结果就是细胞的形状又稳定子的位置决定。所以细胞形状的生成和维护机制同确定细胞最终形状的机制分开(操作系统设计里policy和mechanism分开有点类似)。

    探索过程中测试者不必知道行为提供者。行为提供者也不必知道测试机制。这样的结果是变异和适应非常灵活,因为测试者和行为提供者可以自由发展。反正合者生,不合者死。负面作用就是这种选择代价高昂。自然选择的每一秒都伴随着无数生命的消亡。
  • 隔离和定位
    我们身体的每一坨细胞都源于单一的受精卵。所有细胞的遗传信息(才1GM内存!)都一样。但是,我们有各种专门细胞,比如皮肤细胞,神经细胞,肌肉细胞等。这些细胞再进一步组织成组织,器官,和器官系统。这一切之所以可能,是因为细胞们能根据环境特异化。也就是说,细胞的行为并没有被编入细胞间的信号传递,而是由基因组决定。不同的信号组合可以激活或关闭细胞的某项功能。不同的细胞组合起来,进一步实现更为复杂的功能。

    优秀的软件系统有类似的特点。它们高度模块化。不同的模块在不同的环境里执行对应的功能,进行不同的组合。

  • 防御,修补,和再生
    生物大都能防御异物的攻击,修补缺损的部分,再生死亡的肌体。现在的软件开始包括防御功能,比如安全特性。不过,很少有强大的修补和再生功能,虽然号称自己能自我调控的系统不少。前两年IBM闹腾得欢的autonomous computing最近好像也让位给牛皮轰轰的SOA。Erlang的容错系统倒非常诱人:它的基础设施让系统比较轻松地侦测出问题的进程,然后在不影响系统运行的前提下重启该进程。

  • 组合
    复杂系统都是又小模块组合而成。这点我们并不陌生。我们构建的系统模块依赖事先写好的规范。比如接口的规范,比如接口调用的顺序。这种规范现行的办法随着系统复杂程度的增高变得越来越难。相反,人类的基因组信息不过区区1G,还不够容纳一个普通操作系统的规范,却足以决定我们的构造和行为。我们的“系统接口”必须能够自我配置,适应环境。代价是系统的初始化时间太长,9月怀胎不过是刚开始。

文章也讨论了具备这些通用特性的健壮系统需要哪些基础功能:

  • 通用的部件
    健壮的系统总是由一系列通用部件构成。每类部件都有广泛的应用范围。每个部件能接受范围宽广的输入,但能输出范围狭窄的结果。这好比久经考验的电子原件。他们能在充斥了噪音信号的环境里正确工作,输出可以预测的信号。
  • 可以扩展的泛型操作符。比如同样是加号,+, 既可以处理整数相加,也能处理实数相加,也能处理矩阵相见。更重要的是,还能让用户扩展该操作符的语义,引入新的功能。这样不仅能让新程序容易编写,而且能旧的程序自然增加功能,应对新的环境。这点其实很多语言都有所支持,尤其是现下流行的动态语言。比如说在Lisp环境下开发,我们可以开发一个简单的版本,让程序运行起来。然后我们就在这个运行的程序里不断调试,修改,和加入新的功能,直到这个系统健全。这里有演示录像
  • 生成和测试
    我们的系统应该让我们能够写出返回多项选择的函数,然后同步测试这些结果,挑选出合适的结果。如果结果不好,系统自动回溯。这样做的危险是回溯可能导致指数级的运行时间。不过这也是进化不可避免的代价。其实生成-测试的理念早已用到编程当中。比如Prolog的自动回溯。和众多动态语言支持的模式匹配--比如Erlang, 比如Scala,比如Ocaml。
  • 通过约束得到的普适过程
    这个大概是说通过一系列的约束条件,我们可以构建出对应的约束网络。通过这个网络,我们能生成适应这些约束条件的过程或函数。这样生成的函数能满足广泛的应用环境。
  • 工程中的简并
    作者提出了两种方法。一是利用AI中常用的技巧:目标导向(goal-directed)的方法。这个本质上是说我们不规定系统怎么做一件事,而是告诉系统要达到什么目标。Prolog简直是解决这类问题的天生杀手。当然,现代的函数编程语言也是解决这类问题的利器。二是对同一问题实现多种独立解法,然后让系统挑选合适的方法。

文章还讨论了支持系统健壮和进化的方法:

  • 组合子
    简单说,我们应该通过搭配组合相对独立的部件来构建系统。函数语言对这种编程方式尤其擅长。特别是支持transparent referential integrity和lazy evaluation的函数语言,比如Haskell:我们搭建出基本的模块。Haskell系统提供一大堆强大的黏合工具,让我们把这些模块轻松地组装起来。不过文章也强调,关键还是模块的质量。函数编程只是有效的手段。
  • Continuation
    这个特性在Ruby, Python,Scheme, Smalltak等现代动态语言里都有。Continuation是非常强大的编程手段。一个Continuation对象被创建时能保留当时系统的有关状态,并在其他任意时间被调用。这让程序员获得对时间的直接控制。我们可以暂停某段计算,并在一段时间后继续那段暂停的计算。牛人Avi就利用Smalltak对Continuation的完善支持写出了绝对让人惊叹的Seaside Web编程框架。不信邪的老大们可以去体验以下用Seaside搭建的应用,dabbledb
  • 回溯和并发
    没有回溯,我们就不能自动检验多项选择,排除不合适的结果。没有并发,我们不能同时检验多个结果,计算的代价太高。这些好像不新鲜。
  • 任意联系
    标注元数据便是一个例子。我们不能也不可能预测系统运行时数据间的所有关联。所以构建可以任意扩展的数据关联系统就非常重要了。这好像也不新鲜。比如说至少20年前人们就认识到Metaobject Protocol的重要性,有兴趣的老大可以去读这本经典的书。现在Java支持的Annotation也可以算一个例子。
  • 动态配置的接口
    这个比较新鲜,属于正在研究的难题。现在的方法是让一个群体内的计算实体(agent)通过不断地交流信息来建立大家理解的规则。俺没有看明白。

总之,这篇文章属于高来高去的主题演讲性质的文章,但它对生物系统以及生物系统与计算系统的联系的描述着实让我大开眼界。也许文章结尾是最好的总结:正经的工程不过几千年历史。我们构造健壮系统的手段还远未成熟。我们还没有从漫漫几十亿年生物进化中吸取经验….

你可能感兴趣的:(编程,生物,erlang,python,软件测试)