本文译自 Quora 问题 “What are some myths about functional programming and functional programming languages?” 下的一个高票回答。我已经不记得当初决定翻译它时的心情,不过,经过多次组会,这篇译文终于完成了。
下面是我能想到的几种常见误解,后文会逐一展开:
- “函数式编程”是定义明确的概念
- 函数式编程仅仅是命令式编程的特殊形式
- 函数式编程本来就很难
- 函数式编程本来就是复杂的
- 函数式编程不宜编写图形用户界面
- Haskell 仅仅是函数式的
- 你需要大量的数学知识来进行函数式编程
- 函数式程序员必须非常聪明而且擅长计算机科学
“函数式编程” 是一个(明确的)概念
它不是。它是计算机科学中定义最糟糕的术语之一 —— 甚至不如“面向对象”,但比“声明式编程”好一点点。弄清楚一种语言是否是“函数式的”更像是在做人类学(anthropology)研究,而不是解决计算机科学问题。
无论给出哪一种技术定义,总有人会对“函数式编程”的定义不满。有些定义过于宽泛 —— 如果你想要的是 lambda 表达式,那么从 C++、Java 到 Python 的各种语言突然都变得“函数式”了,只有 C 和汇编不是!如果你要一个数学上的定义,那么只有 Coq、Agda 及其同类语言是函数式的,因为就算是 Haskell 都允许部分函数和非终止性。还有异常。哦,上帝,异常!
就我而言,把每一种带有“脉冲”的语言(即任何具有 lambda 表达式的语言,lambda 的符号为 λ,像一个脉冲)都叫作“函数式语言”,并没有多大用处。当我说“函数式的”,我通常是指 Haskell,或者至少是 ML 这样的语言,而不是 Java、Python、JavaScript,甚至也不是 Common Lisp。Scheme 和 Clojure 有一些函数式特征,不过这里不多谈,尽管我确实有不少使用前者(Scheme)的经验。
好吧,说实话,除非另有说明,我指的就是“Haskell”。这就是我给你们的语言学定义。(译者注:这一句是按我的理解译的。)要是你少花点时间用命令式语言,多花点时间猜我的心思,你就明白了3。
当然,有些人倾向于给“函数式编程”一个更杂糅的定义!
函数式编程仅仅是少了某些东西的命令式编程
这也许是最有害的神话,因为它广泛流传,特别具有误导性,却往往藏在未经明说的假设里。人们随手就把函数式编程理解成穿上紧身衣的“正常”编程。完全不是这样!相反,函数式编程为编程提供了一个新的基础。你会用不同的方式表达计算,而且往往是截然不同的方式。事实上,大多数时候,命令式写法与函数式写法之间并不存在一一对应的关系。
这一点在 Haskell 中最明显,因为它是唯一把函数式特性放在第一位的语言。这实际上让 Haskell 比混合语言明显更有表达能力 —— 从确定性的并行、重写规则(如向量融合)、软件事务性内存(STM,Software Transactional Memory)、惰性计算,到它的大量库,这一切都是因为 Haskell 的函数式核心才成为可能。同时,这也使得 Haskell 与其他语言如此不同。这是一种完全不同的思考方式,一个新的基础 —— 不像许多其他语言,只是同一种香肠换了个型号(宝马也一样5)。
这让我想到另一个相关误解:Haskell 与非纯函数式语言之间的差别,只在于纯度高低,而不在于类型不同。事实绝非如此。根据我的经验,Haskell 的纯函数式特性和惰性计算,使它在实践和设计思想上都与 OCaml、Scala 等语言存在根本差别。多范式语言的倡导者常说,同时支持函数式和命令式编程才是最好的选择。但事实并非如此:在许多方面,Haskell 的表达能力都强于混合语言。
例如,Haskell 中可以很容易地将计算具体化(reify)为数据,从而写出模块化程度更高的代码。这样就能方便地将计算的定义与求值分开。列表通常以数据结构的形式替代循环;树可以表示复杂的递归函数。反过来,这使得任意类型上的 fold 和 unfold 操作在 Haskell 中比在其他语言中更强有力。
在 Scala 这样的语言中尝试模拟 Haskell 风格的函数式编程并不容易。Edward Kmett —— 一位杰出的 Haskell 开源开发者 —— 甚至走得更远:他重新设计了一门 JVM 语言来克服这些限制;可以看一下他对“为何 Scala 不够”这一问题的详细论述。
这里的教训是:引入非纯函数式特性并不是只有好处;它不仅牺牲了大量安全性,也牺牲了表达能力。 当然,非纯函数式特性确实能让一些代码更容易实现,但这些需求大多也能用 Haskell 已有的特性满足,例如状态线程(ST,state threads)或者 IO。
而这些还没有算上前面提到的其他 Haskell 特性,例如向量融合、重写规则和确定性并行;它们同样依赖纯函数式特性或惰性计算。
函数式编程原本就难
函数式编程与大多数人习惯的编程方式差别很大。很多人发现学习函数式编程很难,因为这就像完全重新学习如何编程。回想一下学习第一门编程语言时的感受,再想象自己要重新经历一遍。当然了,这看上去会很难!
从 Java 迁移到 C# 很简单。从 Java 转向 Python 需要稍微调整思路,但基本概念仍然相通。甚至从 C 到 Java 也不太糟 —— 从概念上看,Java 是在与 C 相同的基础上增加新概念,两者都有变量、控制结构和表达式。从你最初学习的语言到当今流行的新命令式语言,这个过程是渐进的。
函数式编程并不是这样,这就像从你脚下抽走地毯一样。最基础的想法都被完全替换掉了。不再有语句(statement),不再有循环(loop),不再有变量。见鬼,不再有程序执行 —— 对于一段函数式程序,你不是运行它,而是对它求值。事实上,对于 Haskell 这样的语言,求值顺序属于更底层的实现细节,不影响程序要表达的行为。执行顺序控制作用(effect)何时发生,而这与表达式如何求值是分开的。这意味着你书写程序的顺序很大程度上不再重要,而这对于命令式程序员来说很奇怪,因为命令式的思考方式要求你时刻将程序的执行顺序记在脑中。
函数式编程本质就是复杂的
人们也常把“复杂”和“困难”当作同一回事。Rich Hickey 的“简单导致容易(Simple Made Easy)”2的演讲精彩地阐述了“复杂”和“困难”的关系。前者(简单还是复杂)是系统的一个属性,粗略地说,就是它有多大。后者是人的一个属性 —— 一件事情有多难很大程度上取决于一个人的经验和所受教育。
有些人确实觉得函数式语言很难。但这并不意味着它是复杂的!事实上,你可以把 Haskell 这样一门语言的核心求值规则和类型规则塞到一张纸上。当然,你必须使用非常精确的数学符号才行,但这也仅在规则比较少时行得通。对 ML 语言也是如此。诚然,任何在现实世界中使用的语言,包括 Haskell,都会很快积累额外的复杂性。但至少,函数式语言依旧可以基于 λ 演算,保持一个最小的、简单的以及良好定义的内核 —— 这是命令式语言不能声称的特性。
这里需要区分实现简单与语义简洁。函数式语言追求的是后者:它们以更复杂的运行时环境或编译器为代价,来追求更加一致的行为。命令式语言(以 Google 的 Go 语言作为一个极端例子)通常采用相反的方针:比起语义的简洁,更加注重实现上的简单。它们选择了不一致和未定义的行为,来换取一种简单的实现,同时希望语言与硬件之间的对应关系更直接。
函数式编程不适合编写图形用户界面(GUI)
函数式编程用来编写 GUI 再合适不过了!我们只是采用不同的方式而已。我们有一个编写 GUI 代码的全新6范式:函数反应式编程(FRP)。FRP 使 GUI 代码更简单,更加模块化,更具声明式特性。
GUI 代码是对时间的建模。使用命令式语言,我们通过可变状态和回调函数间接表示时间。在这里,时间只能算是“二等公民”。这使得我们不能直接谈论时间,只能陷入错综复杂的回调函数,并与高度耦合的全局状态纠缠在一起。当然如果你很小心,这可能只会变成半全局状态。你甚至不能拿一个变量,然后声称“当 x 大于 7 时,让这个变量变红;否则使它变蓝”。相反,你需要大量样板代码和额外结构来把 x 封装在一个模型之中(或者其他什么东西之中),还要带上事件监听器(event listeners)和常用的访问器(accessors)。
而使用 FRP,时间是你可以精确表示出来的东西。这就是我所说的让时间成为“一等公民”:你可以编写代码,直接表达变量值如何随时间变化。
查看什么是函数反应式编程?以及我的生命游戏中的例子 FPR | jelv.is (带有完整的代码Reactive-Life)。
FRP 不仅仅让我们可以编写漂亮的反应式 GUI 代码:它也很适合音乐,甚至机器人领域的应用!在这些场景中,相比使用回调函数和状态,这无疑是向前迈了一步!
Haskell 仅仅是一门函数式语言
不,Haskell 是一门完整的多范式语言。它很容易就能支持命令式编程 —— 人们戏称它为“最好的命令式语言”或者“完美的 Algol”1 —— 以及逻辑式编程。它甚至能够支持面向对象编程(OOP),只是没有多少人关注这一点。Haskell 代码甚至可以看起来像 C 语言,当然要付出一点努力!
唯一的区别是,不同于其他任何一门多范式语言,Haskell 以函数式编程为基础。其他的语言给你一个命令式的基础,然后在此之上叠加函数式的功能。Haskell 给你一个函数式的基础,然后在此之上叠加命令式或者逻辑式的功能。
这既是 Haskell 与其他语言的区别,也是不熟悉 Haskell 的人(non-Haskellers)常常纠结的地方。这也引发了初学者针对函数式语言提出的一堆无意义的讨论,例如“Haskell 为什么不允许值的改变(mutation)”。
你需要大量的数学知识才能使用函数式编程
实际上不需要。当然了,函数式语言是在数学基础上设计的 —— 同时命令式语言是在计算机体系结构基础上设计的。然而,你使用 C 语言时并不需要知道关于 ALU (Arithmetic Logic Unit,计算逻辑单元)的寄存器知识。
我接触函数式编程时,数学知识仅限于基础微积分。此外,我从来都不特别擅长数学,但这并没有妨碍我。在理解相关理论之前,我就已经学会在实践中使用 monads(单子)、applicatives 和 functors(函子)。
事实上,我是通过 Haskell 才学到相关数学知识的。不过,如果你实在不喜欢数学,也不必继续学习这些理论。学习 Haskell 中例如函子和单子的抽象,就像学习 Java bean、Lua 的协程或 Scheme 的宏一样。事实上,函子(Functor)的思想是我最早学到的计算机科学概念之一 —— 它指的就是任何一种允许你在其上映射函数的类型。
函数式程序员必须非常聪明而且擅长计算机科学
无需多言,我是一个函数式程序员 :) 。
函数式编程不是黑暗、邪恶的魔法。(好吧,也许 Coq 是:P)。在许多方面,函数式编程其实能帮你弥补脑力上的不足。Haskell 的类型系统能排除许多错误写法,让你顺着类型的约束,也能一步步解决困难问题。
我发现 Haskell 是边喝酒边编程7时最好的语言。所有愚蠢的错误 —— 以及一些不愚蠢的错误 —— 都可以被编译器捕捉到。所以我可以摆弄我的代码直到类型检查通过。然后,程序(通常)就可以正常工作了。通过类型检查后就能正常工作的概率,高得超出我的预期。Haskell 提供了许多工具,帮你应对自己的不靠谱。
函数式编程对初学者也是出奇地容易上手。例如,在 Jane Street 公司,他们会教所有新入职的交易员使用 OCaml。我承认 Jane Street 公司的交易员是特别聪明的一群人,但是他们中许多人绝不是程序员更不是计算机科学家。但经过短期的 OCaml 集中培训,他们就能高效地工作,其中一些交易员还会花大量时间编写函数式代码。
另一个很好的例子是 IMVU 公司。我的一位朋友在那里的团队工作,帮助不少普通程序员转用 Haskell。在 Web 开发中,这些人同样能在短时间内开始高效工作。冒着听起来傲慢自大的风险,IMVU 这类公司的员工平均智商水平比不上世界上顶级交易公司的员工。
也就是说,我的经验是 Haskell 社区中,聪明且擅长计算机科学的人_确实_占了较高比例。并不是 Haskell 要求你如此,而是这个社区具有相当的自我选择特性。聪明的人似乎更可能主动选择 Haskell。这与不久前 The Python Paradox讨论的现象很相似,而 Python 又被广泛认为是最容易学习的语言之一!
脚注4:
1这很好地体现了 ha ha only serious:有些话乍看只是玩笑,其实另有深意。Haskell 是一个很好的命令式语言,因为它拥有作为一等公民的“命令式动作”(“imperative actions”);你不必将语句封装在 lambda 中来将它们传来传去!这也让我们能够把各种命令式控制结构做成库,从下面的 when,一直到 callCC:
when
to
callCC
这些控制结构因此可以直接作为库来使用。
此外,Jargon File也非常精彩。如果你对计算机科学的发展历程和“黑客”文化感兴趣,它会是一份绝佳的资料。
2这是一次精彩的演讲。我爱他定义的概念框架,即便我不同意他的一些结论。静态类型 —— 尤其是基于 Hindley-Milner、且不支持子类型(subtyping)的类型系统 —— 实际上不复杂。其实在某些方面,它们比使用动态类型的 Clojure 更容易使用!(译者注:竟然黑 Clojure!)
演讲链接:Simple Make Easy
3我仍然认为,这是成为一个有趣的程序员的简单方法,而且过程也很有趣。
4还记得我提到在函数式编程中顺序并不重要?是的,这完全是我记不清脚注顺序时给自己找的借口。毕竟,我内心深处还是一个函数式程序员!
5两种看起来完全不同的型号(前面是 5 系列,后面是 7 系列),大多数编程语言之间的差异也大抵如此。
6实际上,就像函数式编程一样,FRP(函数反应式语言)已经存在一段时间了,至少从 1997 年开始。只是直到最近才开始受到关注。
7我喝酒时用过大量不同的语言,所以我可以做出公平比较。在旧金山这个疯狂的地方,我们靠解答 Project Euler 上的题目和喝酒找乐子。多么美妙的生活啊!