實(shí)用的函數(shù)式編程

函數(shù)式編程 (functional programming) 正式開(kāi)始有長(zhǎng)足的發(fā)展始于 10 年前, 從那時(shí)起, 我開(kāi)始看到 Scala, Clojure 和 F# 這樣的語(yǔ)言得到關(guān)注. 這種關(guān)注并非只是像 "哇, 一個(gè)新語(yǔ)言, 酷!" 這樣短暫的熱度, 而是確實(shí)有某些實(shí)在的原因在推動(dòng)著它 -- 或者至少我們是這么認(rèn)為的.

摩爾定律告訴我們每隔 18 個(gè)月, 計(jì)算機(jī)的速度就會(huì)翻倍. 這個(gè)定律一直從 1960 和 2000 都始終有效. 但是隨后, 它開(kāi)始失效, 慢慢冷卻下來(lái). 時(shí)鐘頻率到達(dá) 3 ghz 以后, 達(dá)到了一個(gè)瓶頸期. 我們已經(jīng)走到了光速的限制. 信號(hào)不能在芯片表面以更高的速度快速傳播。

所以硬件設(shè)計(jì)者改變了策略. 為了獲得更大的吞吐量, 他們添加了更多的處理器 (核心數(shù)). 同時(shí)為了這些核騰出空間, 他們從芯片上移除了很多緩存 (cacheing) 和管道 (pipelining) 硬件. 因而, 處理器的確比之前慢了一點(diǎn), 但是由于有了更多的處理器, 吞吐量仍然得到了增長(zhǎng).

8 年前, 我有了第一臺(tái)雙核機(jī)器. 兩年后我有了一個(gè) 4 核的機(jī)器. 這些核心數(shù)已經(jīng)開(kāi)始不斷增長(zhǎng). 那個(gè)時(shí)候我們都相信, 它將會(huì)以我們無(wú)法想象的方式影響軟件發(fā)展.

于是我們開(kāi)始學(xué)習(xí)函數(shù)式編程 (FP). 一旦變量被初始化后, 函數(shù)式編程強(qiáng)烈不支持再對(duì)變量的狀態(tài)進(jìn)行改變. 這對(duì)并發(fā) (concurrency) 有著深遠(yuǎn)的影響. 如果你無(wú)法改變一個(gè)變量的狀態(tài), 就不會(huì)有一個(gè)競(jìng)爭(zhēng)條件 (race condition). 如果你更新一個(gè)變量的值, 也不會(huì)有并發(fā)更新的問(wèn)題.

當(dāng)然了, 這曾經(jīng)被認(rèn)為是多核問(wèn)題的解決方案. 當(dāng)核心數(shù)激增, 并發(fā), 不止! 共時(shí)性 (simultaneity) 將會(huì)成為一個(gè)非常顯著的問(wèn)題. 函數(shù)式編程應(yīng)該提供一個(gè)編程方式, 這種方式會(huì)減輕在單個(gè)處理器應(yīng)對(duì) 1024 核可能會(huì)出現(xiàn)的問(wèn)題.

所以, 所有人開(kāi)始學(xué)習(xí) Clojure, Scala, F# 或是 Haskell; 因?yàn)樗麄兿嘈藕瘮?shù)式編程終會(huì)大放異彩, 他們想要提前為這一天做好準(zhǔn)備.

然而, 這一天終究沒(méi)有到來(lái). 六年前我有了一個(gè) 4 核的筆記本, 然后我又有了兩個(gè) 4 核. 而我的下一臺(tái)筆記本估計(jì)也是 4 核. 我們又到了另一個(gè)瓶頸期?

說(shuō)個(gè)題外話(huà), 昨晚我看了一部 2007 年的電影. 女主角正在使用一個(gè)筆記本, 使用 Google 在一個(gè)時(shí)髦的瀏覽器里面瀏覽網(wǎng)頁(yè), 使用翻蓋手機(jī)接收信息. 一切是那么熟悉. 不過(guò)這已經(jīng)過(guò)時(shí)了 -- 我可以看出筆記本的模型老舊, 瀏覽器是個(gè)老版本, 翻蓋手機(jī)與今天的智能手機(jī)也實(shí)在是相差甚遠(yuǎn). 然而 -- 這種變化并沒(méi)有從 2000 到 2011 年的那般戲劇化, 也沒(méi)有從 1990 到 2000 年的翻天覆地. 我們又到了在計(jì)算機(jī)和軟件技術(shù)上的一個(gè)瓶頸期了嗎?

所以, 也許函數(shù)式編程并不想我們?cè)?jīng)想象的那么重要. 或許我們不會(huì)被那么多的核心包圍, 也不用去擔(dān)心在芯片上有 32,768 個(gè)核心. 或許我們都可以放松一下, 回到之前更新變量的時(shí)候.

不過(guò), 我認(rèn)為這將會(huì)是一個(gè)重大的錯(cuò)誤, 跟濫用 goto 一樣嚴(yán)重的錯(cuò)誤. 和放棄動(dòng)態(tài)調(diào)度 (dynamic dispatch) 一樣危險(xiǎn)。

為什么呢? 從一開(kāi)始讓我們感興趣的地方開(kāi)始 -- 函數(shù)式編程使得并發(fā)變得十分容易. 如果你要搭建一個(gè)有很多線(xiàn)程或是進(jìn)程的系統(tǒng), 使用函數(shù)式編程將會(huì)大大減少你可能由于競(jìng)爭(zhēng)條件和并發(fā)更新遇到的問(wèn)題.

還有呢? 函數(shù)式編程更易寫(xiě), 易讀, 易于測(cè)試和理解. 聽(tīng)到這些, 相信很多人已經(jīng)開(kāi)始興奮了. 當(dāng)嘗試過(guò)函數(shù)式編程以后, 你會(huì)發(fā)現(xiàn)一切都非常容易. 所有的 map, reduce 和遞歸 -- 尤其是 尾遞歸 , 都非常簡(jiǎn)單. 使用這些只是一個(gè)熟悉程度的問(wèn)題. 一旦你熟悉這些概念以后 -- 并不會(huì)花費(fèi)太長(zhǎng)時(shí)間, 編程會(huì)變得容易的多.

為什么變得容易了呢? 因?yàn)槟悴辉傩枰櫹到y(tǒng)的狀態(tài). 由于變量的狀態(tài)無(wú)法改變, 所以系統(tǒng)的狀態(tài)也就維持不變. 不需要跟蹤的不僅僅是系統(tǒng), 列表, 集合, 棧, 隊(duì)列等通通都不需要再進(jìn)行跟蹤, 因?yàn)檫@些數(shù)據(jù)結(jié)構(gòu)也無(wú)法改變. 在一個(gè)函數(shù)式編程語(yǔ)言中, 當(dāng)你向一個(gè)棧 push 一個(gè)元素, 你將會(huì)得到一個(gè)新的棧, 原來(lái)的棧并不會(huì)發(fā)生改變. 這意味著減輕了程序員的負(fù)擔(dān), 他們所需要記憶的東西更少了, 需要跟蹤的東西更少了. 因而, 代碼會(huì)更易寫(xiě), 易讀, 易于理解和測(cè)試.

那么, 你應(yīng)該使用哪種函數(shù)式編程語(yǔ)言呢? 我最喜歡的是 Clojure. 因?yàn)?Clojure 極其簡(jiǎn)單. 它是 Lisp 的一個(gè)方言, Lisp 是一個(gè)十分簡(jiǎn)單和漂亮的語(yǔ)言. 在這里, 來(lái)稍微展示一下:

在 Java 中的一個(gè)函數(shù): f(x);

現(xiàn)在, 將它轉(zhuǎn)換為 Lisp 的一個(gè)函數(shù), 簡(jiǎn)單地將第一個(gè)括號(hào)移到左邊即可: (f x).

現(xiàn)在, 你已經(jīng)學(xué)會(huì) 95% 的 Lisp 和 90% 的 Clojure 了. 對(duì)這些語(yǔ)言而言, 這些括號(hào)就是全部的語(yǔ)法了. 極其簡(jiǎn)單.

你可能以前見(jiàn)過(guò) Lisp 程序, 不過(guò)不喜歡這些括號(hào). 可能你也不喜歡 CAR, CDR 和 CADR 這些. 別擔(dān)心. Clojure 有著比 Lisp 更多的符號(hào), 所以括號(hào)相對(duì)少一些. Clojure 用 first, rest 和 second 代替了 CAR, CDR 和 CADR. 此外, Clojure 基于 JVM, 它完全可以訪問(wèn) Java 庫(kù), 和任何其他的 Java 框架和庫(kù). 它的互用性快速而便捷. 更好的一點(diǎn)是, Clojure 能夠擁有JVM 完全的面向?qū)ο筇卣?

"等一下!" 你可能會(huì)說(shuō), "函數(shù)式編程和面對(duì)對(duì)象是相互不兼容的!" 誰(shuí)告訴你的? 事實(shí)并非如此! 在函數(shù)式編程中, 你的確無(wú)法改變一個(gè)對(duì)象的狀態(tài). 但是那又怎么樣呢? 當(dāng)你想要對(duì)一個(gè)對(duì)象進(jìn)行改變時(shí), 得到一個(gè)新的對(duì)象就好了, 之前的對(duì)象無(wú)須改變. 一旦你習(xí)慣于此, 這是十分容易處理的.

再回到面向?qū)ο? 我發(fā)現(xiàn)面向?qū)ο笞钣杏玫囊粋€(gè)特性是, 在軟件架構(gòu)層面的動(dòng)態(tài)多態(tài)性. Clojure 提供了對(duì) Java 動(dòng)態(tài)多態(tài)性的完全接入. 最好是用例子解釋一下:

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

上面的代碼定義了一個(gè) JVM 的多態(tài) interface. 在 Java 中, 這個(gè)接口看起來(lái)可能像這樣:

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

在 JVM 這個(gè)層面, 所生成的字節(jié)碼是完全相同的. 實(shí)際上, 一個(gè) Clojure 的寫(xiě)程序要去實(shí)現(xiàn)這個(gè)接口會(huì)像 Java 實(shí)現(xiàn)一樣. 一個(gè) Clojure 程序會(huì)通過(guò)同樣的 token 實(shí)現(xiàn)一個(gè) Java 的 interface. 在 Clojure 中, 看起來(lái)大概像這樣:

(deftype Gateway-imp [db]
  Gateway
  (get-internal-episodes [this]
    (internal-episodes db))

  (get-public-episodes [this]
    (public-episodes db)))

注意構(gòu)造函數(shù)參數(shù) db 和所有的方法是如何訪問(wèn)它的. 在上例中,接口的實(shí)現(xiàn)只是通過(guò)傳遞 db 簡(jiǎn)單地委托給了一些本地函數(shù)。

跟 Lisp 一樣, Clojure 也是一個(gè) 同像性(Homoiconic) 的語(yǔ)言, 也就是說(shuō), 代碼本身就是程序能夠操作的數(shù)據(jù). 這不難看出. 下面的代碼: (1 2 3) 表示一個(gè)三個(gè)整數(shù)的列表 (list). 如果該列表的第一個(gè)元素變成了一個(gè)函數(shù), 也就是 (f 2 3), 那么它就變成了一個(gè)函數(shù)調(diào)用. 故而, 在 Clojure 中, 所有的函數(shù)調(diào)用都是列表. 列表可以直接被代碼操作. 所以, 一個(gè)程序也可以構(gòu)造和執(zhí)行其他程序.

最后說(shuō)一句, 函數(shù)式編程十分重要. 你應(yīng)該去學(xué)習(xí)它. 如果你還在想你應(yīng)該從哪個(gè)語(yǔ)言學(xué)起, 我推薦 Clojure.

本文譯自: Pragmatic Functional Programming

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容