上周末读了 John Backus 的 1977 年图灵奖获奖演讲论文,很受震撼。CS 学科依然很年轻,有些基础性问题探索了四十多年了还没结果。John Backus 在四十多年前提出的思想,放到现在都依然超前。我以前发表过《如何在 JS 代码中消灭 for 循环》,看起来比较离经叛道故意搞争议。没想到消灭 for 循环这件小事,早在四十多年前就有现代编程语言的祖师爷之一明确支持过了……
这篇文章只是对 John Backus 论文的浅显介绍。我也想深度介绍一下,但功底有限。论文中的 formal proof 部分我看不懂,就跳过了;剩下部分比较好懂,但是有些上下文知识我不知道,John 的各种论断无从扩展思考,我就只转述。如果你英文过关,建议直接读原论文。另外,笔者还只是个 CS 小学生,如果有理解偏差,还望各路大神轻喷。
一,John Backus 是谁?
在获得图灵奖之前,John Backus 主要的贡献有这些:
- 他是 Fortran 的作者。Fortran 是第一个被广泛使用的高阶编程语言,比 LISP 还早。后来出现的大量编程语言,很多都有 Fortran 的影子。最近我在读 Douglas Crockford 的新书 How JavaScript Works, Douglas 把 JS 里面一些糟糕的语言特性归罪于 Fortran 的不良影响。
- 他是 Backus–Naur form (BNF) 的主要发明者,这种标记方法为现代编程语言提供形式句法定义。
- 他领头设计了 ALGO 语言。
他在程序语言设计上,以及在自然语言和计算机语言的交互上都有开创性贡献,把他列为现代主流(指令式和过程式)编程语言的祖师爷之一并不为过。
他因为上述贡献而被授予 1977 年图灵奖。然而,在他的获奖演讲里,他否定了自己的成就,并为发明了 Fortran 而感到遗憾……
那篇演讲论文题为 Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs。这种标题放到四十年后的今天估计也会引战的,下面我介绍下这篇论文主要讲了什么。
二,冯诺依曼架构
论文标题里面的 von Neumann Style 指的是受冯诺依曼计算机架构影响的编程风格。冯诺依曼架构是上世纪四十年代,冯诺依曼为了解决当时 ENIAC 计算机的低效而提出的一种架构设计。在冯诺依曼架构下,一台计算机包括:
- 一个包含控制单元和算术/逻辑单元的中央处理器。
- 一个内存单元。
- 一个大型储存单元。
- IO 设备
可以看出冯诺依曼架构是现代计算机的主流架构,目前的计算机也是这种架构。
John Backus 认为,冯诺依曼架构是为了解决四十年代的问题而提出的,这种架构在当时是简洁优雅而高效的。但是到了七十年代,冯诺依曼架构要解决的问题不存在了,继续沿用这种架构会带来一些问题。(笔者注:既然冯诺依曼架构四十年前就过时了,为什么到现在依然是主流?是 John Backus 错了,还是因为 John Backus 后来解释的那样,冯诺依曼架构催生了冯诺依曼编程语言,而后者又绑架了计算机设计者,让非冯诺依曼架构难以诞生?)
John Backus 认为冯诺依曼架构的问题有这些:
一,冯诺依曼瓶颈 (von Neumann Bottleneck)
在冯诺依曼架构下,程序和数据储存在内存里,处理器要通过一个管道来从内存里读取数据,计算完成后再通过管道把结果送回内存。这个管道就是个瓶颈。不管处理器效率多高,计算的速度取决于管道传输数据的速度。
二,冯诺依曼架构桎梏了编程语言
为了适应冯诺依曼架构,编程语言变得臃肿而低效。具体表现有这些:
- 逐字解释编程风格(word-at-a-time style)。例如大家常见的各种 for 循环,while 循环和 break 指令。
- 句法和状态转移的强耦合。在指令式编程里面进行状态管理,通常要在句法层面做各种手动 housekeeping。
- 表达式 (expression) 和语句 (statement) 的分离。表达式是具有代数属性的,代数表达式方便我们理解 (reason about) 程序,并且为程序提供正确性保证。但是在指令式编程里面,这种代数属性被副作用给破坏了。而语句则无序,不提供有用的数学属性,只用来逐步执行指令。
- 不提供有效的组合形式。无法基于现有程序构建新的程序。注意,这篇论文发表在 1977 年,那时还没有面向对象这个概念。我猜,如果 John Backus 知道面向对象,他也不会赞成那种程序组合方式。
三,传统编程语言的问题
下面继续深入前面提到的冯诺依曼风格语言的问题。
John Backus 认为,赋值语句就是语言上的冯诺依曼瓶颈。在冯诺依曼瓶颈里传输的数据,很大一部分根本不是有效数据,而是这些数据的名字,以及用来计算这些名字的操作指令和数据。这无疑是低效的。更重要的是,这种编程风格带来的思维的瓶颈,让我们只能在逐字解释的框架下去思考,不鼓励我们用更大的概念单元 (conceptual unit) 去思考编程问题。(这个论断当下依然有效)
进一步,John Backus 指出
Our fixation on von Neumann languages has continued the primacy of the von Neumann computer, and our dependency on it has made non-von Neumann languages uneconomical and has limited their development. The absence of full scale, effective programming styles founded on non-von Neumann principles has deprived designers of an intellectual foundation for new computer architectures.
如果这个论断是正确的,John Backus 解释了在他发表这篇论文四十多年后,我们还没有突破冯诺依曼瓶颈的原因。
当时 LISP 已经诞生了,但是 John Backus 认为 pure LISP 还是带着一堆传统编程特征的扩展。
在这篇论文里面,John Backus 专门挑了 for 循环来说明冯诺依曼语言的问题。(我要是早点知道就好了 ^_^) 他给的例子如下:
c := 0
for i := 1 step 1 until n do
c := c + a[i]xb[i]
复制代码
我不知道这是什么语言,不过很明显这是个 for 循环。John Backus 如下分析上面这段 for 循环的问题:
a) Its statements operate on an invisible "state" according to complex rules. 根据复杂的规则去操作看不到的状态。
b) It is not hierarchical. Except for the right side of the assignment statement, it does not construct complex entities from simpler ones. (Larger programs, however, often do.) 程序没有层级,没有组合。
c) It is dynamic and repetitive. One must mentally execute it to understand it. 指令动态而重复,读代码的人要在脑子里执行一遍才懂。
d) It computes word-at-a-time by repetition (of the assignment) and by modification (of variable i). 通过重复和改变值来逐字解释。
e) Part of the data, n, is in the program; thus it lacks generality and works only for vectors of length n. 数据和程序强耦合,难以复用。
f) It names its arguments; it can only be used for vectors a and b. To become general, it requires a procedure declaration. These involve complex issues (e.g., call-by-name versus call-by-value). 给变量命名,使程序不够通用。John Backus 的这个主张后来即使在函数式语言的探索里也没被采纳。这里我不太懂。
g) Its " housekeeping" operations are represented by symbols in scattered places (in the for statement and the subscripts in the assignment). housekeeping 可以理解为具体的程序执行。for 循环把这些执行指令散布在各处,难以巩固成可通用的,单一的操作符。
针对上面提到的问题,John Backus 提议的语法如下:
Def Innerproduct
= (Insert +) . (ApplyToAll x) . Transpose
复制代码
上面的等号是三个横线的,并不是赋值语句。
用 JS 可以借助 Ramda 翻译如下:
const Innerproduct = R.compose(
R.sum,
R.map(R.apply(R.multiply)),
R.transpose,
)
复制代码
Ramda 函数的具体实现也是冯诺依曼风格的,另外赋值操作在 John Backus 的提议里也是禁止的,这里就忽略了,只看高阶抽象部分。
John Backus 认为第二种风格的优势如下:
a) It operates only on its arguments. There are no hidden states or complex transition rules. 引用透明,只操作参数。没有隐藏状态,没有复杂的状态转移规则。
b) It is hierarchical, being built from three simpler functions and three functional forms. 有层级,可组合。
c) It is static and nonrepetitive, in the sense that its structure is helpful in understanding it without mentally executing it. 静态,非重复。不用在脑子里执行一遍程序,仅看程序结构也能理解程序意图。
d) It operates on whole conceptual units, not words; it has three steps; no step is repeated. 以完整的概念单元为操作对象,而不是逐词解释。三个步骤各自独立而不重复。
e) It incorporates no data; it is completely general; it works for any pair of conformable vectors. 程序不包含数据,完全通用。我理解的函数式编程的 point free 应该符合这里的描述。
f) It does not name its arguments; it can be applied to any pair of vectors without any procedure declaration or complex substitution rules. 不给参数命名。如上所述,目前多数语言没有做到,这里不做探讨。
g) It employs housekeeping forms and functions that are generally useful in many other programs; in fact, only + and × are not concerned with housekeeping. These forms and functions can combine with others to create higher level housekeeping operators. 程序指令通用而可组合,可以组合成更高阶的操作符。
四,冯诺依曼架构的替代
John Backus 最后提到了他那个时代一些替代冯诺依曼架构的尝试,如 applicative machine,这种架构模型没有储存器和地址寄存器,也就没有冯诺依曼瓶颈。John Backus 认为 applicative 风格程序比冯诺依曼程序更强大。这部分我不懂,就不多展开了。
John Backus 的这篇论文发表之后,学术界开始活跃探索函数式编程,Haskell 的诞生就受此影响。不过,替代冯诺依曼架构的方案目前好像还远不够成熟。语言层面上,四十多年来有了更多的突破冯诺依曼风格桎梏的尝试,但距离催生新的计算机设计架构还很遥远。
插个广告
蚂蚁保险体验与社区技术组招高级前端开发工程师。有兴趣的同学联系 ray.hl@alipay.com