譯 | 實用的函數語言程式設計

原文連結: ofollow,noindex" target="_blank">Pragmatic Functional Programming – Robert C. Martin (Uncle Bob) ,2017-07
基於 liuchengxu 的譯文稿: 實用的函數語言程式設計
譯序
Bob
大叔的短文, FP
在軟體開發優點上務實的思考,引導大家理解、學習和使用 FP
,文章後半篇還用 FP
語言 Clojure
簡約演示了一番。在文末不忘呼籲學習 FP
,並推薦 Clojure
語言。
務實的函數語言程式設計
函數語言程式設計( Functional Programming
/ FP
)的風潮講真大概是從10年前開始。我們開始關注像 Scala
、 Clojure
和 F#
這樣的語言。這個風潮並非只是『哇酷~一門新語言!』這樣平平常常的熱情。確實有些實在的原因在推動 —— 或者我們是這麼想的。
摩爾定律告訴過我們,每隔18個月計算機的速度就會翻倍。這個定律從60年代到2000年都是有效的,但之後失效了。大家冷靜想一下。時鐘頻率到達了3G HZ,進入了平臺期。這已經觸及了光速的物理極限,而在晶片表面上的訊號傳播速度限制住了計算速度的提升。
所以硬體設計師改變了策略。為了獲得更大的吞吐量,在晶片上添加了更多的處理器(核心數);同時為了給新加的核騰出空間,在晶片上去掉了很多快取( cacheing
)和流水線( pipelining
)硬體。結果是,單個處理器較之前要慢一些;但是由於有了更多的處理器,吞吐量仍然是提升的。
【譯註】:關於流水線( pipeline
)參見 指令流水線( instruction pipeline
) – zh.wikipedia.org 。
8年前我有了第一臺雙核機器,兩年後有了一臺4核的機器。也就是說,核數開始進入了激增的時期。那時候我們都認識到,這將會以我們無法想象的方式影響軟體開發。
一個對策就是學習 FP
。 FP
強烈不鼓勵在變數( variable
)初始化之後再改變其狀態( state
)。這對併發( concurrency
)有著深刻影響。如果不能改變變數的狀態,就不會有競爭條件( race condition
)。如果不能更新變數的值( value
),也就不會有併發更新( concurrent update
)的問題。
當然了,這一直被認為是多核問題的解決方案。當核數激增,併發,甚至是 共時性 ( simultaneity
),都會成為一個大問題。 FP
可以說是提供了一種程式設計風格( the programming style
),可以減輕當一個處理器中有1024核時所出現的問題。
所以所有人都開始學習 Clojure
、 Scala
、 F#
或是 Haskell
;因為大家相信衝鋒號即將吹響,都想提前做好準備!
然而,這一天一直沒有到來。六年前我有了一臺4核的筆記本,比上一臺多了兩個核。而我的下一臺筆記本估計還會是4核。我們又進入了另一個平臺期?
說個題外話,昨晚我看了一部2007年的電影。女主角在用筆記本在一個時髦的瀏覽器裡面瀏覽網頁、使用 Google
、用翻蓋手機收發簡訊。一切都是那麼的熟悉。只不過都過時了 —— 我可以看出筆記本是老型號,瀏覽器是個老版本,而翻蓋手機比起今天的智慧手機就像個古董。儘管如此,這些變化卻比不上從2000到2011年這段時間的變化那麼翻天覆地。更是遠比不上從1990年到2000年間的變化。難道我們正在見證計算機和軟體技術發展的平臺期嗎?
所以,或許, FP
並不是一個我們之前想的那樣關鍵的技能。或許,我們將不會被那麼多的核包圍。或許,也不用去擔心會有32768個核的晶片。或許,我們都可以放鬆一下,又回到以前那樣去更新變數。
但是,我覺得這會是一個錯誤,一個嚴重的錯誤。我覺得這個錯誤會和濫用 goto
一樣嚴重。我覺得這會和放棄動態派發( dynamic dispatch
)一樣危險。
【譯註】:關於動態派發( dynamic dispatch
)參見 動態排程( dynamic dispatch
) – zh.wikipedia.org 。
為什麼呢?讓我們回到 FP
起初引起我們興趣的原因上 —— FP
使得併發變得安全得多。如果你要搭建一個有很多執行緒或是程序的系統,使用 FP
會大大減少可能由競爭條件和併發更新而引發的問題。
還有其它理由嗎?呃~
FP
更易寫、易讀、易於測試和易於理解。聽到這些,我能想象到,有些讀者比如你已經掀桌子咆哮了。你嘗試過 FP
,『有毛容易啊』是你的感受。 map
、 reduce
和遞迴 —— 尤其是 尾 遞迴,有哪個說得上容易?你說得沒錯,收到收到。但是,這其實只是個是否熟悉的問題。一旦你熟悉了這些概念以後(建立起熟悉的過程其實並不需要太長時間),程式設計就會變得 更容易得多 。
為什麼會更容易呢?因為你不再需要跟蹤系統的狀態。由於變數的狀態無法改變,所以系統的狀態也就維持不變。不需要跟蹤的不僅僅是系統,還有列表、集合、棧、佇列等等通通都不需要跟蹤狀態,因為這些資料結構也無法改變。在 FP
語言中,當你向一個棧 push
一個元素,你將會得到一個新的棧,並不會改變原來的棧。這意味著,像接拋球雜技那樣,程式員在空中需要同時控制好的球變少了;需要記憶的東西更少了;需要跟蹤的東西更少了。因而程式碼的編寫、閱讀、理解和測試變得簡單得多。
那麼,你應該使用哪種 FP
語言呢?我最喜歡的是 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
框架和庫。與 Java
互操作性快速而便捷。更好的是, Clojure
能夠使用 JVM
所有的面向物件功能。
『等一下!』你可能會說,『 FP
和麵向物件是相互不相容的!』誰告訴你的?胡說八道!在 FP
中,你的確無法改變一個物件的狀態。但是那又怎樣呢?就像 push
一個整數進棧後會返回一個新的棧,當呼叫一個方法調整一個物件的值時,返回的是一個新物件而不是改變原來的物件。一旦你習慣了這樣的做法,處理起來是很容易的。
再回到面向物件。我覺得面向物件最有用的一個特性,在軟體架構層面,是動態多型性( dynamic polymorphism
)。 Clojure
提供了對 Java
動態多型性的完全的使用能力。舉個例子解釋起來可能最方便:
(defprotocol Gateway (get-internal-episodes [this]) (get-public-episodes [this]))
上面的程式碼定義了一個 JVM
的多型介面。在 Java
中,這個介面看起來可能像這樣:
public interface Gateway { List<Episode> getInternalEpisodes(); List<Episode> getPublicEpisodes(); }
在 JVM
層面所生成的位元組碼是完全相同的。實際上, Java
寫的程式可以實現這個介面,就像這個介面是用 Java
寫的一樣。對等的, Clojure
程式也可以實現一個 Java
寫的介面。看起來大概這個樣子:
(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)
表示一個三個整數的列表。如果該列表的第一個元素變成了一個函式,也就是 (f 2 3)
,那麼它就變成了一個函式呼叫。可見,在 Clojure
中所有的函式呼叫都是列表,而列表可以直接被程式碼操作。所以,一個程式也可以構造和執行其它的程式。
最後說一句, FP
是重要的。你應該去學習。如果你還在糾結要用哪個語言來學 FP
,我推薦 Clojure
。