实用的函数式编程

函数式编程 (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 动态多态性的完全接入. 最好是用例子解释一下:

(defprotocol Gateway
  (get-internal-episodes [this])
  (get-public-episodes [this]))

上面的代码定义了一个 JVM 的多态 interface. 在 Java 中, 这个接口看起来可能像这样:

public interface Gateway {
    List<Episode> getInternalEpisodes();
    List<Episode> getPublicEpisodes();
}

在 JVM 这个层面, 所生成的字节码是完全相同的. 实际上, 一个 Clojure 的写程序要去实现这个接口会像 Java 实现一样. 一个 Clojure 程序会通过同样的 token 实现一个 Java 的 interface. 在 Clojure 中, 看起来大概像这样:

(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容