漫談 JS 函數語言程式設計(一)

發表於2017-06-27

這可能是最簡單易懂的函數語言程式設計介(扯)紹(淡)了

目前前端界(以及其他一些領域)對函數語言程式設計大體上兩種態度,一些人是覺得函數語言程式設計特牛逼,尤其是現在許多新生的框架和庫都在標榜自己的函式式特徵。而另一些人,又覺得函數語言程式設計學起來很難,而且似乎也沒有什麼卵用,理由是在自己經歷的專案裡面很難看到具體的函數語言程式設計應用場景,甚至其中許多人認同一個觀點,覺得函數語言程式設計只適合於學術研究,很難在工程專案中實際使用。

不管你在閱讀本文之前屬於哪一種人,又或者你是剛接觸函數語言程式設計的新人,都沒有關係。本文不是研究函數語言程式設計正規化的學術研究,而函數語言程式設計作為一個可以說是程式設計理論中最古老的程式設計正規化,在它幾十年上百年的發展歷史中,已經積累了大量的資料和素材,對於想要在學術領域裡完全弄明白它的同學,完全可以在網上、書店裡找到各種資料。本文的重點不在於概念,而在於實戰。因此,你不會聽到太多各種函數語言程式設計的名詞討論,比如諸如 Curry、Mond 之類的專業術語。相反,我們主要來討論函數語言程式設計在前端領域內使用的一些實際例子,瞭解為什麼前端需要學習函數語言程式設計,使用函數語言程式設計寫程式碼能給我們帶來什麼。如果弄明白了這些,那麼關於函數語言程式設計不實用的謠言也就不攻自破了。


資料抽象或過程抽象

為什麼我們接受程式導向或物件導向思想很容易,而我們要完全接受函數語言程式設計卻感覺難得多?

我認為這個問題大體上可以這麼解釋:

人腦本能地容易理解“看得見“、“摸得著”的物體,對於“運動”和“變化”一類不著形的東西,人腦理解起來要略微地費勁一些。而人類要做好一件複雜的事情,大腦有兩種抽象方向,一種是對實體進行抽象,另一種是對過程進行抽象:

簡答來說,即在軟體設計的過程中,如果要保證軟體產品的功能穩定可用,同時要保證它的靈活性和可擴充套件性,那麼系統就要有變化的部分和不變的部分。哪些部分應當設計成“不變”,哪些部分應當設計成“可變”,在這個取捨過程中,FP(函數語言程式設計)和 OOP(物件導向程式設計)正是走了兩條不同的路線。

物件導向對資料進行抽象,將行為以物件方法的方式封裝到資料實體內部,從而降低系統的耦合度。而函數語言程式設計,選擇對過程進行抽象,將資料以輸入輸出流的方式封裝進過程內部,從而也降低系統的耦合度。兩者雖是截然不同,然而在系統設計的目標上可以說是殊途同歸的。

物件導向思想和函數語言程式設計思想也是不矛盾的,因為一個龐大的系統,可能既要對資料進行抽象,又要對過程進行抽象,或者一個區域性適合進行資料抽象,另一個區域性適合進行過程抽象,這都是可能的。資料抽象不一定以物件實體為形式,同樣過程抽象也不是說形式上必然是 functional 的,比如流式物件(InputStream、OutputStream)、Express 的 middleware,就帶有明顯的過程抽象的特徵。但是在通常情況下,OOP更適合用來做資料抽象,FP更適合用來做過程抽象。

純函式

再具體深入下去之前,我們先來解答一個問題,那就是為什麼用 FP 或過程抽象能夠降低系統的耦合度。這裡我們要先理解一個概念,這個概念叫“純函式”。

根據定義,如果一個函式符合兩個條件,它被稱為純函式:

  • 此函式在相同的輸入值時,總是產生相同的輸出。函式的輸出和當前執行環境的上下文狀態無關。
  • 此函式執行過程不影響執行環境,比如不會觸發事件、更改環境中的物件、終端輸出值等。

簡單來說,也就是當一個函式的輸出不受外部環境影響,同時也不影響外部環境時,該函式就是純函式。

JavaScript 內建函式中有不少純函式,也有不少非純函式。

比如以下函式是純函式:

  • String.prototype.toUpperCase
  • Array.prototype.map
  • Function.prototype.bind

以下函式不是純函式:

  • Math.random
  • Date.now
  • document.body.appendChild
  • Array.prototype.sort

為什麼要區分純函式和非純函式呢?因為在系統裡,純函式與非純函式相比,在可測試性、可維護性、可移植性、平行計算和可擴充套件性方面都有著巨大的優勢。

在這裡我用可測試性來舉例:

對於純函式,因為是無狀態的,測試的時候不需要構建執行時環境,也不需要用特定的順序進行測試:

對於非純函式,就比較複雜:

函數語言程式設計能夠減少系統中的非純函式

首先我們看一個例子:

JS Bin on jsbin.com

在這裡我們有兩個彼此依賴的非純函式,setColor(el, color) 和 setColors(els, color)。在測試的時候,我們需要構建環境來測試兩個函式。

現在,我們用函數語言程式設計思想來改造這個系統:

JS Bin on jsbin.com

在這裡,我們建立一個過程抽象的高階函式 batch(fn),這個函式的作用是,對它的輸入函式返回一個新的函式,這個函式與輸入函式的區別是,如果呼叫的第一個實參是一個陣列,那麼將這個陣列展開,用每一個值依次呼叫輸入函式,返回一個陣列,包活每次呼叫返回的結果。

batch(fn) 本身雖然看似複雜,但是有意思的事,這個函式無疑是純函式,所以 batch(fn) 自身的測試是非常簡單的:

由於我們上面舉的例子 setColor 和 setColors 雖然不是純函式,但是卻非常簡單,因此似乎設計 batch(fn) 的意義不大,有把系統變得更復雜的嫌疑。然而,對於有許多操作 DOM 的函式的框架或庫,有了 batch(fn),我們就可以實現很簡單的介面(對單一元素操作),然後利用 batch(fn) 獲得更復雜介面(對元素進行批量操作),從而大大降低系統本身的複雜的,提升可維護性。

注意一點,batch(fn) 輸出的函式有副作用,然而 batch(fn) 用閉包將輸出的函式的副作用限制在了 batch(fn) 的作用域內。

Ramda.js 的 lift 方法

Ramda.js 的 lift 方法和 batch 有一點點類似,不過功能更強大。讓我們來用它實現一個有一點點“燒腦”的效果,來作為這篇文章的結尾:

JS Bin on jsbin.com

— 期待下一篇吧 —

相關文章