网络埋伏纪事

组合软件:2. 为什么要在 JavaScript 中学习函数式编程?

网络埋伏纪事 · 2017-05-02翻译 · 1820阅读 原文链接

请忘掉你认为你知道的有关 JavaScript 的任何东西,以初学者心态来接触这份资料。为帮助你这样做,我们打算从头开始复习 JavaScript 的基础知识,就好像你以前从来没有看到过 JavaScript 一样。如果你是初学者,那就走运了。最后从零开始探索 ES6 和 函数式编程!希望所有新概念在这个过程中都被解释到了 - 但是别指望会太舒适。

如果你是一个已经熟悉 JavaScript 或者纯函数式语言的老手,也许你会认为用 JavaScript 来探究函数式编程简直个笑话。请把这些想法放一边,试着用开放的心态来接触这份资料。你可能会发现这是另一个层级的 JavaScript 编程。你从未知道它的存在。

既然本文是称为“组合软件”,而函数式编程是组合软件的显而易见的方式(使用函数组合、高阶函数等等),你可能会想知道为什么我不讨论 Haskell、ClojureScript 或者 Elm,而是 JavaScript。

JavaScript 有函数式编程所需的最重要的特性:

  1. 一等函数:即将函数用作数据值的能力:传递函数为参数、返回函数以及将函数复制给变量和对象属性。这个特性允许高阶函数,从而能启用偏应用、柯里化和组合。
  2. 匿名函数和简洁的 lambda 语法x => x * 2 在 JavaScript 中是有效的函数表达式。简洁的 Lambda 让它更容易与高阶函数配合。
  3. 闭包:闭包是函数与其词法环境捆绑在一起。闭包是在函数创建时创建的。当一个函数被定义在另一个函数内部时,它可以访问外层函数中绑定的变量,即使在外层函数退出之后也是如此。闭包是偏函数应用获取其固定参数的手段。固定参数是一个绑定在一个被返回的函数的闭包作用域中的参数。在 add2(1)(2) 中,1 就是被 add2(1) 返回的函数中的固定参数。

JavaScript 缺什么

JavaScript 是一种多范式语言,就是说它支持多种不同风格的编程。JavaScript 支持的风格包括过程式(命令式)编程(像 C 一样,函数代表可以被反复重用和组织的指令子程序),面向对象编程(对象,不是函数,是基本构造单元),当然还有函数式编程。多范式语言的缺点是命令式和面向对象编程趋向于暗示几乎所有东西都需要是可变的。

变动(Mutation)是指发生在原地的对数据结构的改变。例如:

const foo = {
  bar: 'baz'
};

foo.bar = 'qux'; // mutation

对象通常需要是可修改的,这样其属性就可以通过方法来修改。在命令式编程中,大多数数据结构都是可修改的,以保证高效的对象和数组的原地操作。

如下是一些函数式语言有,但是 JavaScript 没有的特性:

  1. 纯度:在有些函数式编程语言中,纯度是通过语言强制的。带有副作用的表达式是被禁止的。
  2. 不可变性:一些函数式语言禁用了变动。表达式被求值为新的数据结构,而不是修改已有的数据结构,比如数组或者对象。这可能听起来效率低下,不过很多函数式语言在幕后使用字典树数据结构,而字典树这种结构是以结构性共享为特征:这意味着旧对象和新对象共享引用相同的数据。
  3. 递归:递归是函数为迭代用途引用自身的能力。在很多函数式语言中,递归是迭代的唯一方法。没有像 forwhile 或者 do 循环这样的循环语句。

纯度:在 JavaScript 中,纯度必须按约定实现。如果不是通过组合纯函数来创建大多数应用程序,就不是用函数式风格编程。很不幸的是,在 JavaScript 中通过偶然创建和使用非纯函数很容易偏离轨道。

不可变性:在纯函数语言中,不可变性通常是被强制的。JavaScript 缺乏被大多数函数式语言所用的高效、不可变的基于字典树的数据结构,不过有些库可以帮助实现,包括 Immutable.jsMori。希望将来的 ECMAScript 版本规范会包含不可变数据结构。

有带来希望的迹象,比如 ES6 中添加的 const 关键字。用 const 定义绑定的名称不能再次赋值引用不同的值。理解 const 并不代表不可变值很重要。

const 声明的对象不能被重新赋值来引用一个完全不同的对象,不过它应用的对象可以有可修改的属性。JavaScript 还能 freeze() 对象,不过这些动向只在根级别上是冻结的,就是说嵌套的对象依然可以有可修改的属性。换句话说,在 JavaScript 规范中我们要看到真正的组合不可变,依然有很长一段路要走。

递归:JavaScript 从技术上讲是支持递归的,不过大多数函数式语言有一个称为尾调用优化的特性。尾调用优化是让递归函数能重用递归调用的栈帧。

如果没有尾调用优化,调用栈就会无休止增长,从而导致栈溢出。JavaScript 在 ES6 规范中从技术上讲是有一个有限形式的尾递归优化。不幸的是,只有一个主流浏览器引擎实现了它,并且优化只是部分实现了,然后后来从 Babel(最热门的标准 JavaScript 编译器,用于将 ES6 编译为 ES5,以便在较老的浏览器中使用)中删掉了。

结果:对于大的迭代,使用递归依然是不安全的 - 即使你很小心在尾位置调用函数。

什么是 JavaScript 有的,而纯函数语言没有的

语言纯正癖者会告诉你,JavaScript 的可变性是其主要缺陷,这是真的。不过,副作用和可变动有时候是有好处的。实际上,创建大多数有用的现代应用程序要想没有副作用是不可能的。像 Haskell 这种纯函数式语言也要用到副作用,只不过是使用称为 Monads 的盒子将副作用伪装成纯函数,从而让程序保持纯,即使被 monad 表示的副作用是不纯的。

Monad 的麻烦是,即使其用法很简单,但是对哪些对很多示例不熟悉的人解释什么是 Monad,有点像向盲人解释蓝色看起来像什么一样。

“一个单子(Monad)说白了不过就是自函子(Endofunctor)范畴上的一个幺半群(Monoid)而已,这有什么难以理解的?” ~ James Iry, fictionally quoting Philip Wadler, paraphrasing a real quote by Saunders Mac Lane. “编程语言简史(伪)”

通常,模仿夸大了的事情会让一个笑点更搞笑。在上面的引言中,Monad 的解释实际上是对原始引言做了简化。原始引言是:

"X 中的一个单子只是 X 的自函子范畴上的一个幺半群,积 x 被自函子的组合替换,单元集合被 indentity 自函子替换。 "数学工作者必知的范畴学"

即使是这样,在我看来,害怕 monad 的论证很无力。学习 Monad 的最佳方式不是去读一堆该主题的书和博文,而是直接开始用它。如同函数式编程中的大多数事情一样,费解的学术词汇比概念更难理解。相信我,为理解函数式编程,你不必理解桑德斯·麦克兰恩。

虽然 JavaScript 对于每种编程风格可能都不是完全理想,但是它无可争辩的是一种被设计用来可以被不同编程风格和背景的不同人群所使用的通用语言。

根据 Brendan Eich 所言,这从一开始就是有意而为之的。那时,网景必须支持两类程序员:

“…组件程序员,他们用 C++ 或者(我们希望的)Java 编写组件;以及业余或者专业的脚本编写者,他们编写的代码要直接嵌入到 HTML 中。”

最初,网景的意图是要支持两种不同的语言,而脚本语言可能会类似于 Scheme(一种 Lisp 的方言)。Brendan Eich 又说:

“我被招募进网景是他们答应我可以在浏览器中玩 Scheme。”

然而,JavaScript 必须是一门新语言:

来自高层工程管理人员的命令是:这门语言必须看起来像 Java。这实际上就已经把 Perl、Python、Tcl 以及 Scheme 排除在外。”

所以,从一个开始,Brendan Eich 脑子中的想法就是:

  1. 浏览器中的 Scheme。
  2. 看起来像 Java。

它最终就成了一个大杂烩:

“我并非骄傲,只不过是很高兴我选择 Scheme 式的一等函数以及 Self 式(尽管很怪异)的原型作为主要因素。至于 Java 的影响,主要是把数据分成基本类型和对象类型两种(比如字符串和 String 对象),以及引入了Y2K 日期问题,这真是不幸。 ”

我把最终进入 JavaScript 中的一些”不幸“类似 Java 的特性加入到如下列表中:

  • 构造器函数和 new 关键字,从工厂函数有不同的调用方式和用法语义。
  • class 关键字加上单一祖先的 extend 作为主要的继承机制。
  • 用户的偏好是把一个 class 当作是一个静态类型(实际上它不是)。

我的建议是:尽可能避免这些玩意。

幸运的是,JavaScript 已经结束了成为这样一种包罗万象的语言,因为事实证明脚本的方式已经完胜“组件”的方式(今天,Java、Flash 和 ActiveX 插件已经不被大多数安装了的浏览器所支持)。

最终只有一种语言直接被浏览器所支持:JavaScript。

也就是说浏览器不再臃肿而且 bug 更少,因为它们只需要支持一套语言绑定的设置:JavaScript 的。你可能会想 WebAssembly 是一个例外,不过 WebAssembly 的设计目标之一是用一种兼容的抽象语法树(AST)来共享 JavaScript 的语言绑定。事实上,将 WebAssembly 编译为 JavaScript 的子集的第一次示范就是 ASM.js。

作为 Web 平台上唯一的标准通用编程语言的地位,让 JavaScript 顺势成为软件历史上最流行的语言:

App 赢得了世界,web 赢得了 app,而 JavaScript 赢得了 web。

多种 评测表明, JavaScript 现在是世界上最热门的编程语言。

JavaScript 并非是函数式编程最理想的工具,不过它一个在大型、分布式团队上创建大型应用程序的极佳工具,而不同的团队对于如何创建一个应用程序会有不同的想法。

有些团队会专注于脚本粘合,这时命令式编程特别有用。其它一些团队会专注于创建架构的抽象,这时一点(有节制的,小心谨慎的) OO 的思想可能不是一个坏主意。还有一些其他团队会拥抱函数式编程,对确定性的、可测试的应用程序状态管理采用纯函数来减少用户行为。这些团队中的成员都是使用同一语言,也就是说他们可以更容易交换想法,相互学习,并且在在彼此的工作基础上做事。

在 JavaScript 中,所有这些想法都能共存,让更多人可以拥抱 JavaScript,从而已经导致了世界上最大的开源包注册库(到 2017 年 2 月为止)npm的出现。

JavaScript 的真正威力在于生态系统中思想和用户的多元化。对于函数式编程语言的纯正癖者来说,它也许不是绝对理想的语言;不过,对于来自其它流行语言(比如 Java、Lisp 或者 C)的人来说,要在一起工作,必须要熟悉可以工作在你可以想像的几乎所有平台上的一种语言,那么 JavaScript 可能是理想的语言。JavaScript 对于有这些语言背景的用户来说,不会感到很舒服,不过他们会对学习该语言并快速提高工作效率感到足够舒服。

我赞同 JavaScript 不是函数式程序员的最佳语言。不过,没有任何其它函数式语言可以宣称它是一种每个人都可以使用和拥抱的语言,正如 ES6 所展示的:JavaScript 能变得更擅长于服务于对函数式编程有需求的用户。与其抛弃被世界上几乎所有公司所用的 JavaScript 以及不可思议的生态系统,为什么不拥抱它,并且逐步让它成为用于软件组合的一种更好的语言呢?

现在 JavaScript 已经是一种足够好的函数式编程语言,也就是说人们正在 JavaScript 中用函数式编程技术创建各种有用和有趣的东西。Netflix(以及用 Angular 2 以上创建的每个应用)使用基于 RxJS 的函数式工具。Facebook 在 React 中用了纯函数、高阶函数和高阶组件的概念来创建 Facebook 和 Instagram。PayPal、KhanAcademy 和 Flipkart 用 Redux 来做状态管理。

它们并不孤独:Angular、React、Redux 和 Lodash 都是 JavaScript 应用程序生态系统中领先的框架和库,并且它们都是深受函数式编程的影响 — 或者以 Lodash 和 Redux 为例,它们构建出来的明确目的就是在真正的 JavaScript 应用程序中启用函数式编程范式。

为什么要用 JavaScript?因为 JavaScript 是大多数真正的公司正用来创建真正的软件的语言。不管你爱它,还是恨它,JavaScript 已经把最流行的函数式编程语言的头衔从旗手 Lisp 那儿偷走了。是的,对于函数式编程概念来说,Haskell 现在是更适合的旗手,不过人们就是没用 Haskell 创建多少真正的应用程序。

任何时候,在美国有近十万个,在全球有数十万个 JavaScript 职位空缺。学习 Haskell 会教你很多有关函数式编程的知识,不过学习 JavaScript 会教你很多创建真实工作中的产品应用的知识。

App 赢得了世界,web 赢得了 app,而 JavaScript 赢得了 web。

相关文章