父母心
首发于父母心

函数式编程的早期历史

有个叫 David Hilbert (希尔伯特)的德国人,1900 年在巴黎数学家大会上抛出了 10 个世纪难题,会后又加了一堆,合称 Hilbert 23 problems

其中Hilbert 第 10 问题 (指的是 23 问题清单上的第 10 个),一直悬而未决。1928 年在 意大利波伦亚举行的数学家大会上,Hilbert 和 Wilhelm Ackermann (阿克曼)发表了 "Principles of Mathematical Logic",其中 Hilbert 10th 问题给出了更为形式化的描述,细化成三个子问题:

First, was mathematics complete ... Second, was mathematics consistent ... And thirdly, was mathematics decidable?" (Hodges p. 91, Hawking p. 1121)

时年 22 岁的 Kurt Gödel (哥德尔)在会上听了 Hilbert 的演讲后,就决定拿这个问题做博士论文了。1929 年他完成了论文,证明了一阶谓词演算中所有逻辑上有效的公式都是可以证明的。这个一阶谓词演算完备性,也就被称为哥德尔完备性定理。然后,1930 年他顺利地在维也纳拿到了博士学位。

1930 年 9 月 8 日,在德国柯尼斯堡召开了一次三个科学社区的联合会议。已经 68 岁、被哥廷根(Göttingen)大学强制退休的 Hilbert 做开幕致辞,也是他的退休演讲。这次会议第三天的一个圆桌讨论上 Kurt Gödel 阐述了他的不完备性定理,解决了前两点,并于次年发表了题为“On Formally Undecidable Propositions in Principia Mathematica and Related Systems I”的论文,正如名字所示,他当时想着要写个 II 的,不过从来没真正动手写。

在这部长篇大论里,Gödel 首先证明了一个形式系统中的所有公式都可以表示为自然数,并可以从自然数反过来得出相应的公式。这对于今天的程序员都来说,数字编码、程序即数据计算机原理,这些最核心、最基本的常识,在那个时代却是脑洞大开的创见。运用这个编码系统,Gödel 证明在系统 T 内可以形式化定义命题 P (我在系统 T 内不可被证明),从而给确定性问题给出了否定回答。(过程太啰嗦,想了解细节请看刘未鹏的长文)

第3点则被称为 Entscheidungsproblem (发音 [ɛntˈʃaɪ̯dʊŋspʁoˌbleːm], 是 'decision problem' 的德文说法)。要解决 Entscheidungsproblem 问题,首先要提供 effective computation 的数学定义。

Entscheidungsproblem 引起了许多数学家的兴趣。其中有两个现在广为人知,一个叫 Alan Turing (图灵),一个叫 Alonzo Church (丘奇),两人在同在1936年独立给出了否定答案。

Alan Turing 1935 年跑去听 Max Newman (纽曼)讲“数学的基础”高级课程,从而迷上了确定性问题,并发明了图灵机模型来定义有效计算。Universal turing machine 模型是一个通用计算机模型,这是划时代的进步。图灵机隐含了存储计算的机器模型,这深刻的影响了 Manchester、EDVAC、ENIAC 等机器的建造者,图灵本人后来还亲自操刀了 ACE 机器的设计。总之,基于存储计算的通用计算机由此发源,然后才有了日后兴旺发达的编程行业。

而 Church 在 1933 年就搞出来一套以纯λ演算为基础的逻辑,以期对数学进行形式化描述,他学生 Stephen Kleene 和J. Barkley Rosser 随后提出 Kleene–Rosser 悖论,证明最初的系统不自洽。好在纯λ演算部分是自洽的,Kleene 试图在λ演算系统里重现 Gödel 不完备定理时,搞定了用 λ 演算来表达算术和递归函数。这些成绩将 λ演算的研究引向了λ可定义性(definability)。

1934 年 Gödel 来到普林斯顿访问。3 月时,Church 向他提出可以用 λ可定义来表达有效运算的猜想, Gödel 对此深表怀疑。1931 年 Gödel 在不完全定理论文中已经引了入原始递归函数(primitive recursive function), 这时候他进一步将其一般化成为通用递归函数,加上了两个约束而使之成为有效的(effective),形成了通用递归模型。

由于 Gödel 1934 年演讲时表示核心概念来自 Jacques Herbrand (厄布朗)在 4 月 7 日 信件里跟他探讨的内容,所以称为 Gödel-Herbrand recursive function。本来这里有一段凄美的故事,丫拖到当年 7.25 才给 Herbrand 回信,可惜 Herbrand 7.27 就在 Alps 登山时意外身亡了,1932 年 Herbrand 前一年提交的论文在身后发表。不过呢,最早那封信 1986 年被人翻出来了,2004年 CMU 的 Wilfried Sieg 研究后发现,貌似 Gödel 记错了,信里面讨论的其实是 Gödel 第二不完备定理与 Hilbert 形式系统的冲突,这个通用递归系统极有可能是 Gödel 自己折腾出来的 (cmu.edu/dietrich/philos)。不过,这倒不影响 Herbrand 的天才啦,虽然 23 岁就英年早逝,但发的两篇论文已经对数学的基础做出根本性的贡献,有用他名字命名的定理。那为毛 Wilfried Sieg 后来要专门写个文公布这事儿呢?因为他 1994 年写了篇文章说 Herbrand 跟 Gödel 之间这桩轶事,还老被人引用,这谁能受得了呀。

而 Gödel 对 Church 的λ演算系统的怀疑,也促使 Church 师徒去证明任何递归函数都是都是 λ可定义的,这就是 λ可定义模型。1935 年 Church 向数学界宣布他的论题时其实还不是很确定,直到 1936 年发表的文章才正真确认了这一论点。

简单说来,λ演算是一个更符合数学公式表达习惯的系统,因此在 1960 年代之前,这套东东主要是数学家们玩,跟计算机编程没啥关联。而 Gödel 更欣赏 Turing 的模型,是因为图灵机对计算的表达更加直接。

1935/36 这个时间段上,我们有了三个有效计算模型:通用图灵机、通用递归函数、λ可定义。Rosser 1939 年正式确认这三个模型是等效的。

值得一提的是,图灵 1936 年 5.28 向"Proceedings of the London Mathematical Society"提交了论文"'On Computable Numbers, with an application to the Entscheidungsproblem",不过因为 Church 在美国早一步发表了"An unsolvable problem in elementary number theory",这篇对基础数学和计算机两个领域都极其重要的文章发表得并不顺利。尽管 Newman 费劲巴拉的解释两者的方法完全不同,图灵还是不得不修改论文,加了个附录引述了 Church 的工作,并证明两者是等价的,1936 年 11 月 12 日正式提交,1937 年才得以付印出版。在修改过程中,图灵了解到普林斯顿那边儿除了 Church 还有 Gödel, Kleene (克莱尼), Rosser (罗瑟) 以及 Bernays (奈斯)等一堆逻辑学研究者,于是兴冲冲跑去当 graduated student,去了才失望的发现除了 Church 其他人前一年都离开了。而 Church 也就当收了个挂单和尚,所以两个人关系并不亲密。图灵在这里拿到了博士学位,学位论文里意外的提到了一个不动点组合子Θ,这是第一个公开发表的不动点组合子。

前边已经多次提到函数,这全都是数学意义上的函数,最早的定义可以追溯到 1889 年 Giuseppe Peano (皮亚诺),尽管对函数和类(指变量值的集合)的抽象符号一直在发展,但都没有对 substitution 和 convension 提供形式化定义,要等到 1920 年代组合子逻辑和更晚的 λ演算出现时才给出。

Hilbert 研究组里的 Schönfinkel 1920 年 12.7 演讲中就已经提出了组合子逻辑(combinatory logic),1924 年他的同事 Heinrich Behmann (贝曼)整理成文发表了。不过他后来回了莫斯科,1927年发现他因为精神疾病而在住院治疗,只在 1929 发过一篇论文,最终在 1942 年贫病而死。二战恶劣的生存环境下邻居拿他的研究手稿去烧火取暖…… 一直等到 Curry Haskell (柯里) 1927 在普林斯顿大学当讲师时,才重新发现了 Moses Schönfinkel 关于组合子逻辑的成果。Moses Schönfinkel 的成果预言了很多 Curry 在做的研究,于是他就跑去哥廷根大学与熟悉 Moses Schönfinkel 工作的 Heinrich Behmann、Paul Bernays 两人一起工作,并于 1930 年以一篇组合子逻辑的论文拿到了博士学位。

Curry Brooks Haskell 整个职业生涯都在研究组合子,实际开创了这个研究领域,λ演算中用单参数函数来表示多个参数函数的方法被称为 Currying (柯里化),虽然 Curry 同学多次指出这个其实是 Schönfinkel 已经搞出来的,不过其他人都是因为他用了才知道,所以这名字就这定下来了;后来有三门编程语言以他的名字命名,分别是:Curry, Brooks, Haskell。

Curry 在 1928 开始开发类型系统,他搞的是基于组合子的 polymorphic,Church 则建立了基于函数的简单类型系统。

程序正确性证明和程序验证,它的一些基本概念和方法是 40 年代后期诺伊曼和图灵等人提出的。诺伊曼等在一篇论文中提出借助于证明来验证程序正确性的方法。后来图灵又证明了一个子程序的正确性。不过图灵的这一成果长期不受重视。

通用计算机诞生之后,大家都发现直接折腾机器码实在是没效率了,于是搞出了符号汇编语言,但是表达逻辑还是太不直接了。于是 1952 年 Halcombe Laning 提出了直接输入数学公式的设想,并制作了 GEORGE 编译器演示该想法。受这个想法启发,1953 年 IBM 的 John Backus 组建了一支强大的队伍开始给 IBM 704 主机研发数学公式翻译系统,1954 年中发布了规范草稿,并在 1956 年 10 月编制出使用手册,第一个 FORTRAN (FORmula TRANslating 的缩写)编译器 1957.4 正式发行。FORTRAN 程序的代码行数比汇编少 20 倍,尽管大家怀疑编译器生成代码的性能会不如手写(那会儿机时很贵),还是迅速流行开了。

FORTRAN 的成功,让很多人认识到直接把代数公式输入进电脑是可行的,并开始渴望能用某种形式语言直接把自己的研究内容输入到电脑里进行运算。

John McCarthy 1956 年在 Dartmouth 的一台 IBM 704 上搞人工智能研究时,就想到要一个代数列表处理(algebraic list processing)语言。他的项目需要用某种形式语言来编写语句,以记录关于世界的信息,而他感觉列表结构这种形式挺合适,既方便编写,也方便推演。做为 IBM plane geometry 项目的顾问,他先忽悠 Herbert Gelernter 和 Carl Gerberin 给 FORTRAN 加上了 list processing,搞出了 FLPL (FORTRAN List Processing Language),并在该项目中成功应用。

** 正因为是在 IBM 704 上开搞的,所以 LISP 的表处理函数才会有奇葩的名字: car/cdr 什么的。其实是取 IBM704 机器字的不同部分,c=content of,r=register number, a=address part, d=decrement part (iwriteiam.nl/HaCAR_CDR.)

不过 1957~58 年间亲自拿 FORTRAN 写了个国际象棋程序之后,觉得还是太麻烦,于是他 58 年整个夏天都泡在 IBM 的信息研究部琢磨新的语言,以免去自己人工将列表结构翻译成 FORTRAN 程序的麻烦。这种新的编程语言有很多创造性的特性,John McCarthy 提到:

* 用条件表达式来写递归函数 (Church 是用高阶函数的)

* 使用 Church 1941 的符号来表示函数,以便将函数做为参数传递

* 因为看不懂 Church 书里其余的部分,所以就不掂记着实现他那个更通用的函数定义方式了

(也就是除了用 lambda 关键字,其实 最早的 LISP 不是基于 λ演算实现的,理论基础是 Kleene 的一阶递归函数,不过现在的 LISP 实现都是基于 lambda calculus 的了)

这个列表处理语言就叫 LISP。等等,好不容易想得挺美了,可是 IBM 的家伙们对 FLPL 已经很满意了,居然忽悠不动了!

1958 秋天,John McCarthy 在 MIT EE 混上了 Assistant Professor,跟数学系的 Marvin Minsky 一起立了个人工智能的项目,开始自己实现 LISP 。这段故事很长,自己去看吧,这里就提一个,数学上的函数应该结果只跟传入的参数有关,不受其它状态的影响,除了返回值也不影响其它地方的状态,这叫没有副作用,但是实际程序里副作用却能极为方便的提升执行效率,纠结呀,最后 LISP 里还是有副作用的。不过有数学洁癖的这帮家伙后来(1976~1978)论证了 Pure LISP 是可以不支持副作用的,这样才能方便的用一阶谓语逻辑去证明程序的性质。

John McCarthy 对图灵机当然也是很熟悉的,他还很得意的指出, 要显摆 LISP 比图灵机简洁,实现一下 universal LISP function 就明白了嘛,比 universal Turing machine 要简洁好理解就对了。这个就是 LISP 的 eval 函数啦。结果为了实现 eval,Nathaniel Rochester 先发明了 LABEL,以便可以用 LISP 数据来表示 LISP 函数,D.M.R. Part 接着指出用 Church 的 Y 算子其实光用 λ 不用 LABEL 也能搞定。然后, S.R. Russell 发现,这个 eval 实现完全可以当 LISP 解释器嘛。

LISP 开发者们机智的采用了逐步 bootstrap LISP 的实现策略,后来又有了解释器,最后 Timothy Hart and Michael Levin 拿 LISP 写出了第一个能工作的 LISP 编译器,这也是世界上第一个用被编译语言写的编译器。LISP 影响很大,后来衍生出 CommonLisp, Scheme 等许多方言。1978 年对自由变量绑定问题(FUNARG)的分析,让大家认识到元编程与高阶编程是不一样的,这也是 LISP 的一大贡献。

不过,尽管 LISP 各种牛叉,在数字计算上比 FORTRAN 慢 10 到 100 倍也是要了命了。而且,递归式的表达式,看起来也没有命令式的直观易懂。所以,在工程应用方面应用比较有限。

1955 年开始,搞算法研究的人们就在探讨能直接书写算法的编程语言,欧美的计算机科学家们 1958 年在 ETH Zurich 开了个会,建立了一个委员会来定义一种通用高级编程语言,称为 ALGOL (ALGOrithmic Language) ,1958 年首次发表的报告称为 ALGOL58。1959 年在巴黎又开了次会,定下了 ALGOL60,这个才真正有实现,后来还有个 ALGOL68 不过没人鸟,所以今天说 ALGOL 就是指 ALGOL60。

John Backus 为了形式化的描述 ALGOL58 语法规范,发明了 Backus normal form,Peter Naur 在描述 ALGOL60 时进行了修改和扩展,后来 Donald Knuth 建议改名为 Backus-Naur normal form,简称 BNF范式,搞文法的都少不了要用到的。Naur 等在研究 Label 实现时搞出来 program point 的概念,后来演变成了 continuation。Niklaus Wirth 1966年发展了 ALGOL W,因为 IBM 觉得太超前没要,他后来改吧改吧变成了 Pascal 。

ALGOL 是相当超前的,这是第一个结合了命令式和 λ演算的编程语言。尽管一般不认为 ALGOL 是函数式语言,但是它确实实现了 call-by-name,Naur 1964 更订版 ALGOL60 规范精确定义了过程调用的效果,跟 λ演算的 β-reduction 完全一致,它的变量绑定也跟 λ 的规则很相近,1964 年 Landin 给实现了 closure 。

从 1964 年的"The Mechanical Evaluation of Expressions"开始,Landin 写了一系列论文研究编程语言与 λ演算之间的关系,并发明了第一个函数式抽像机器 SECD(1965 Correspondence between ALGOL 60 and Church's Lambda-notation I/II),并从 66 年的The Next 700 Programming Languages"开始阐述他理想中的编程语言 ISWIM(if you see what I mean),基本上就是sugared lambda + assignment + control 。

John Backus 在1970年代搞了 FP 语言,1977 年发表。虽然这门语言并不是最早的函数式编程语言,但他是 Functional Programming 这个词儿的创造者, 1977 年他的图灵奖演讲题为(Can programming be liberated from the von Neumann style?: a functional style and its algebra of programs)。

1973 年 Robin Milner 的研究组在 Edinburgh University 搞了 ML (Meta Language) 用以研究 LCF(Logic for Computable Functions),后来演变出 OCaml 和 Standard ML。[From LCF to HCL](cl.cam.ac.uk/~mjcg/pape)

1974 年 MIT 的 Barbara Liskov 带着学生搞出了 CLU 语言,这个语言带有 cluster (早期的类实现)、异常处理机制、iterator、最早的 generator (yield)。

76年左右,大家开始关注 Laziness。

而 1987 年从美国俄勒冈州波特兰(in Portland, Oregon)召开的函数式编程语言与计算架构会议(FPCA '87 )开始,建立函数式编程开放标准的运动开始了,形成了 Haskell,不过只是书面规范,要到 1990 才开始有实现。


参考资料:(参考了太多,有一些随手记过来了,更多的当时没有记,现在也懒得再一一找回来了,见谅。)

文章被以下专栏收录