函数式编程 (functional programming) 正式开始有长足的发展始于 10 年前, 从那时起, 我开始看到 Scala, Clojure 和 F# 这样的语言得到关注. 这种关注并非只是像 "哇, 一个新语言, 酷!" 这样短暂的热度, 而是确实有某些实在的原因在推动着它 -- 或者至少我们是这么认为的.
摩尔定律告诉我们每隔 18 个月, 计算机的速度就会翻倍. 这个定律一直从 1960 和 2000 都始终有效. 但是随后, 它开始失效, 慢慢冷却下来. 时钟频率到达 3 ghz 以后, 达到了一个瓶颈期. 我们已经走到了光速的限制. 信号不能在芯片表面以更高的速度快速传播。
所以硬件设计者改变了策略. 为了获得更大的吞吐量, 他们添加了更多的处理器 (核心数). 同时为了这些核腾出空间, 他们从芯片上移除了很多缓存 (cacheing) 和管道 (pipelining) 硬件. 因而, 处理器的确比之前慢了一点, 但是由于有了更多的处理器, 吞吐量仍然得到了增长.
8 年前, 我有了第一台双核机器. 两年后我有了一个 4 核的机器. 这些核心数已经开始不断增长. 那个时候我们都相信, 它将会以我们无法想象的方式影响软件发展.
于是我们开始学习函数式编程 (FP). 一旦变量被初始化后, 函数式编程强烈不支持再对变量的状态进行改变. 这对并发 (concurrency) 有着深远的影响. 如果你无法改变一个变量的状态, 就不会有一个竞争条件 (race condition). 如果你更新一个变量的值, 也不会有并发更新的问题.
当然了, 这曾经被认为是多核问题的解决方案. 当核心数激增, 并发, 不止! 共时性 (simultaneity) 将会成为一个非常显著的问题. 函数式编程应该提供一个编程方式, 这种方式会减轻在单个处理器应对 1024 核可能会出现的问题.
所以, 所有人开始学习 Clojure, Scala, F# 或是 Haskell; 因为他们相信函数式编程终会大放异彩, 他们想要提前为这一天做好准备.
然而, 这一天终究没有到来. 六年前我有了一个 4 核的笔记本, 然后我又有了两个 4 核. 而我的下一台笔记本估计也是 4 核. 我们又到了另一个瓶颈期?
说个题外话, 昨晚我看了一部 2007 年的电影. 女主角正在使用一个笔记本, 使用 Google 在一个时髦的浏览器里面浏览网页, 使用翻盖手机接收信息. 一切是那么熟悉. 不过这已经过时了 -- 我可以看出笔记本的模型老旧, 浏览器是个老版本, 翻盖手机与今天的智能手机也实在是相差甚远. 然而 -- 这种变化并没有从 2000 到 2011 年的那般戏剧化, 也没有从 1990 到 2000 年的翻天覆地. 我们又到了在计算机和软件技术上的一个瓶颈期了吗?
所以, 也许函数式编程并不想我们曾经想象的那么重要. 或许我们不会被那么多的核心包围, 也不用去担心在芯片上有 32,768 个核心. 或许我们都可以放松一下, 回到之前更新变量的时候.
不过, 我认为这将会是一个重大的错误, 跟滥用 goto
一样严重的错误. 和放弃动态调度 (dynamic dispatch) 一样危险。
为什么呢? 从一开始让我们感兴趣的地方开始 -- 函数式编程使得并发变得十分容易. 如果你要搭建一个有很多线程或是进程的系统, 使用函数式编程将会大大减少你可能由于竞争条件和并发更新遇到的问题.
还有呢? 函数式编程更易写, 易读, 易于测试和理解. 听到这些, 相信很多人已经开始兴奋了. 当尝试过函数式编程以后, 你会发现一切都非常容易. 所有的 map, reduce 和递归 -- 尤其是 尾递归 , 都非常简单. 使用这些只是一个熟悉程度的问题. 一旦你熟悉这些概念以后 -- 并不会花费太长时间, 编程会变得容易的多.
为什么变得容易了呢? 因为你不再需要跟踪系统的状态. 由于变量的状态无法改变, 所以系统的状态也就维持不变. 不需要跟踪的不仅仅是系统, 列表, 集合, 栈, 队列等通通都不需要再进行跟踪, 因为这些数据结构也无法改变. 在一个函数式编程语言中, 当你向一个栈 push 一个元素, 你将会得到一个新的栈, 原来的栈并不会发生改变. 这意味着减轻了程序员的负担, 他们所需要记忆的东西更少了, 需要跟踪的东西更少了. 因而, 代码会更易写, 易读, 易于理解和测试.
那么, 你应该使用哪种函数式编程语言呢? 我最喜欢的是 Clojure. 因为 Clojure 极其简单. 它是 Lisp 的一个方言, Lisp 是一个十分简单和漂亮的语言. 在这里, 来稍微展示一下:
在 Java 中的一个函数: f(x)
;
现在, 将它转换为 Lisp 的一个函数, 简单地将第一个括号移到左边即可: (f x)
.
现在, 你已经学会 95% 的 Lisp 和 90% 的 Clojure 了. 对这些语言而言, 这些括号就是全部的语法了. 极其简单.
你可能以前见过 Lisp 程序, 不过不喜欢这些括号. 可能你也不喜欢 CAR, CDR 和 CADR 这些. 别担心. Clojure 有着比 Lisp 更多的符号, 所以括号相对少一些. Clojure 用 first, rest 和 second 代替了 CAR, CDR 和 CADR. 此外, Clojure 基于 JVM, 它完全可以访问 Java 库, 和任何其他的 Java 框架和库. 它的互用性快速而便捷. 更好的一点是, Clojure 能够拥有JVM 完全的面向对象特征.
"等一下!" 你可能会说, "函数式编程和面对对象是相互不兼容的!" 谁告诉你的? 事实并非如此! 在函数式编程中, 你的确无法改变一个对象的状态. 但是那又怎么样呢? 当你想要对一个对象进行改变时, 得到一个新的对象就好了, 之前的对象无须改变. 一旦你习惯于此, 这是十分容易处理的.
再回到面向对象. 我发现面向对象最有用的一个特性是, 在软件架构层面的动态多态性. Clojure 提供了对 Java 动态多态性的完全接入. 最好是用例子解释一下:
代码语言:javascript复制(defprotocol Gateway
(get-internal-episodes [this])
(get-public-episodes [this]))
上面的代码定义了一个 JVM 的多态 interface. 在 Java 中, 这个接口看起来可能像这样:
代码语言:javascript复制public interface Gateway {
List<Episode> getInternalEpisodes();
List<Episode> getPublicEpisodes();
}
在 JVM 这个层面, 所生成的字节码是完全相同的. 实际上, 一个 Clojure 的写程序要去实现这个接口会像 Java 实现一样. 一个 Clojure 程序会通过同样的 token 实现一个 Java 的 interface. 在 Clojure 中, 看起来大概像这样:
代码语言:javascript复制(deftype Gateway-imp [db]
Gateway
(get-internal-episodes [this]
(internal-episodes db))
(get-public-episodes [this]
(public-episodes db)))
注意构造函数参数 db 和所有的方法是如何访问它的. 在上例中,接口的实现只是通过传递 db 简单地委托给了一些本地函数。
跟 Lisp 一样, Clojure 也是一个 同像性(Homoiconic) 的语言, 也就是说, 代码本身就是程序能够操作的数据. 这不难看出. 下面的代码: (1 2 3)
表示一个三个整数的列表 (list). 如果该列表的第一个元素变成了一个函数, 也就是 (f 2 3)
, 那么它就变成了一个函数调用. 故而, 在 Clojure 中, 所有的函数调用都是列表. 列表可以直接被代码操作. 所以, 一个程序也可以构造和执行其他程序.
最后说一句, 函数式编程十分重要. 你应该去学习它. 如果你还在想你应该从哪个语言学起, 我推荐 Clojure.
本文译自: Pragmatic Functional Programming