漫长的几年当中社区里讨论 JavaScript 和函数式编程的声音很多, 我无法的详细的去追踪, 也不是本文重点. 鼓吹 JavaScript 函数式编程的大的声音, 我的印象里主要是两次.
第一轮是经常能看到社区当中引用的 Douglas Crockford 在 2001 年写的文章. 后来常用的 Underscore 也继承了类似的思路来发挥函数式编程的一些好处. JavaScript 设计之初借鉴了 Scheme 的一些策略, 将函数作为一等公民, 支持被灵活传递使用. 以及有词法作用域以及闭包这些函数式编程的基础性结构. 这些赋予了 JavaScript 极大的灵活度通过函数来模拟各种需求.
The World's Most Misunderstood Programming Language
JavaScript's C-like syntax, including curly braces and the clunky for statement, makes it appear to be an ordinary procedural language. This is misleading because JavaScript has more in common with functional languages like Lisp or Scheme than with C or Java. It has arrays instead of lists and objects instead of property lists. Functions are first class. It has closures. You get lambdas without having to balance all those parens.
第二轮是 React 触发到大量对于函数式编程的思考, 同期发生的还有 Elm 的 FRP 方案在社区引起巨大反响, 以及 Om 社区反馈到 React 社区一些技术和概念. 当中重要的概念有纯函数和不可变数据. 在 React 的渲染模型当中的, Store updates 和 Component rendering 两个过程需要隔离副作用以保证自由地复用, 而不可变数据则通过结构共享提供了性能优化的方案.
这些观点, 给人的感觉是 JavaScript 很适合函数式编程, 比如自带的数组操作方法常常能串联出比较漂亮的写法, 而且 React 在社区就算不能通吃, 但是已经取得了如此广泛的影响, 让大量的开发者接受了 reducer 纯函数这样的观念, 并在组件抽象上用于很多函数式编程的手法, 逐渐构建了强大的技术栈. 最终, 通过这些来验证 JavaScript 在函数式编程使用上的成功, 某种程度上算是自圆其说了, 而且也做出了成绩.
但是这种理解从不同的角度观察, 还是存在问题的. 我从比较早就接触到了 CoffeeScript 以及深刻影响到它的语言: Haskell. 到现在, 我有三年多 CoffeeScript 开发的经验, 一年的 ClojureScript 小项目的经验, 以及勉强入门的 Haskell 学习经验. 站在 JavaScript 之外, 看到的情况跟在 JavaScript 社区内部看到的并不一样.
首先 Wiki 上的定义, 可以看两点, 1) 用数学函数类似的表达式来定义计算过程, 而不是用汇编那样指令来描述计算, 2) 函数结果严格依赖于它的输入, 其他的影响结果的因素比如可变状态, 是要消除掉的:
Functional programming
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions[1] or declarations[2] instead of statements. In functional code, the output value of a function depends only on the arguments that are input to the function, so calling a function f twice with the same value for an argument x will produce the same result f(x) each time. Eliminating side effects, i.e. changes in state that do not depend on the function inputs, can make it much easier to understand and predict the behavior of a program, which is one of the key motivations for the development of functional programming.
如果详细看 Wiki 会发现信息量非常大, 涉及到的编程语言有几十种, 而且还有"纯函数语言"的分类把某些语言划分出来, 而 JavaScript 被划分到了非函数式编程语言的一类里边叫做 "Functional programming in non-functional languages". 我接触过的函数式语言, 大致分类是 ML 系(Standard ML, OCaml, Haskell), Scheme 系(Racket, Guile, Chicken), CommonLisp 系(CommonLisp, EmacsLisp), Erlang 系(Erlang, Elixir), 还有特立独行的 Scala, Shen 之类的. 当你去了解函数式编程的时候, JavaScript 其实根本没有位置.
函数式编程的深度广度挺复杂, 特别是在后端, 知名的例子比如 Facebook 用 Haskell 解决垃圾邮件过滤的性能问题, 或者 Clojure 作者的数据库 Datomic 的整体设计. 我作为前端开发者, 很难说出具体的细节来. 但显然不是前端用级联写法组合函数以及写写高阶函数那么简单.
所以换个角度来看待一些方面:
这篇文章是为了明确说明 JavaScript 在函数式编程方面支持太少. 我不能从具体 Haskell 代码去解释, 那么换个办法, 按照概念来对比来看 JavaScript 做了什么. 下面的概念我参照一篇文章上的, 以 Clojure 还有 Haskell 为参照. 由于 Haskell 是函数式编程圈子里教科书式的语言, 基本上概念就是遵照 Haskell 罗列的: On Functional Programming
大致做个总结, 就是 Haskell 当中的类型系统, 不可变数据, 控制副作用, 在 Clojure 当中只是做了不可变数据, 同时稍微控制了一下副作用, 而这些概念在 JavaScript 当中很少有支持. 这样的结果, JavaScript 写出来的代码几乎都是不符合函数式编程的限制得.
不可变数据对程序的直接影响就是 for/while 没法写了. 可以想象一下, 如果你代码当中不让写可变数据, 这会是多大的影响, 会极大地影响了代码编写和开发的习惯的. 因为我们通常需要可变的状态来完成通信, 而且还要以 for/while 作为结构来构造程序, 抛开可变状态大学里学的内容很多都用不了了. 思维方式的转变, 是个不小的挑战.
同时也要注意, 函数式编程用的说法是"隔离副作用", 而不是说"去掉"副作用. 比如在 Clojure 当中, 要用共享可变状态的场景, 就要明确声明数据类型是 Atom, 更新数据用到的函数也不一样, 结果是实际使用当中会很有意识地去思考哪些地方直接用尾递归就写完了, 哪些迫不得已要使用 Atom 类型, 这种把可变状态明确区分来的意识在 Clojure 当中经常有. 还有就是比如 IO 这样的副作用, Clojure 当中虽然限制, 但是很松散, 即便写了编译器也不会说什么. Haskell 类型系统强制要求隔离好副作用, 不过我觉得对于大部分开发者来说这样既复杂又多此一举.
与之形成鲜明对比, JavaScript 设计时完全不在乎这些约定, 即便是模仿了 Scheme, 当年 PLT Scheme 那样的语言, 本身也没有限制好数据 immutable(目前 Racket 数据支持 mutable 和 immutable 两种形态, 也是神奇), 也只用了 `f!` 写法来标明副作用, 到了 JavaScript 连副作用都不标记. 结果说来说去, JavaScript 真正和函数式编程搭上的, 也就是闭包和函数一等公民嘛.
而且原本在函数式编程当中, 返回结果只是和参数改变有关, 每个数值又是引用透明的, 即便要做大量的抽象也能放心去做, 不担心出错. 到了 JavaScript 当中, 由于函数可以混用可变数据, 另外加上 this 指针的用法, 经过高阶函数抽象之后, 整个代码可能会变得难以预测, 这样函数式编程的可靠性就无法得到保障了. JavaScript 确实算是学到了函数式编程的技巧具备了灵活性, 但是却很难达到 Clojure 那样的可靠性, 甚至某些情况说不准因为函数抽象而引发更加麻烦的局面.
所以, 我的结论就是, JavaScript 学了几招厉害的, 确实能干点厉害的事情, 但是, 距离把功夫练好还差太远.
这篇文章我主要是吐槽 JavaScript 宣传函数式编程在误导人. 我很多时间在跟进着 Clojure 社区, 对于 Haskell 我只能在边上围观, 我能看到的就是函数式编程水真的很深, 我的文章当中很可能有不准确的地方, 看到的话请评论指出.
来源: http://www.open-open.com/lib/view/open1480644988156.html